mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-28 16:09:17 +00:00
Merge pull request #149 from Bacon2994/codex/redis-rdm-consistency
feat(redis): 修复全库键浏览与二进制展示
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { computed, ref, onMounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Search, RefreshCw, Key, Loader2 } from "lucide-vue-next";
|
||||
import { Search, RefreshCw, Key, Loader2, ChevronRight, ChevronDown, FolderClosed, FolderOpen } from "lucide-vue-next";
|
||||
import { Splitpanes, Pane } from "splitpanes";
|
||||
import "splitpanes/dist/splitpanes.css";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -10,6 +10,12 @@ import { Badge } from "@/components/ui/badge";
|
||||
import RedisValueViewer from "./RedisValueViewer.vue";
|
||||
import * as api from "@/lib/api";
|
||||
import type { RedisKeyInfo } from "@/lib/api";
|
||||
import {
|
||||
buildRedisKeyTree,
|
||||
collectExpandedGroupIds,
|
||||
flattenVisibleRedisKeyTree,
|
||||
type RedisKeyTreeNode,
|
||||
} from "@/lib/redisKeyTree";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -18,23 +24,51 @@ const props = defineProps<{
|
||||
db: number;
|
||||
}>();
|
||||
|
||||
const keys = ref<RedisKeyInfo[]>([]);
|
||||
const flatKeys = ref<RedisKeyInfo[]>([]);
|
||||
const treeKeys = ref<RedisKeyTreeNode[]>([]);
|
||||
const loading = ref(false);
|
||||
const searchPattern = ref("*");
|
||||
const selectedKey = ref<string | null>(null);
|
||||
const selectedKeyRaw = ref<string | null>(null);
|
||||
const cursor = ref(0);
|
||||
const hasMore = ref(false);
|
||||
const expandedGroupIds = ref<Set<string>>(new Set());
|
||||
|
||||
const PAGE_SIZE = 200;
|
||||
|
||||
const effectivePattern = computed(() => searchPattern.value.trim() || "*");
|
||||
const isSearchMode = computed(() => effectivePattern.value !== "*");
|
||||
const selectedKey = computed(() => flatKeys.value.find((key) => key.key_raw === selectedKeyRaw.value) ?? null);
|
||||
const visibleRows = computed(() => flattenVisibleRedisKeyTree(treeKeys.value, expandedGroupIds.value));
|
||||
|
||||
function rebuildTree(expandAll = false) {
|
||||
const nextTree = buildRedisKeyTree(flatKeys.value, props.db);
|
||||
treeKeys.value = nextTree;
|
||||
|
||||
const nextExpanded = new Set<string>();
|
||||
const availableExpanded = collectExpandedGroupIds(nextTree);
|
||||
if (expandAll) {
|
||||
for (const id of availableExpanded) nextExpanded.add(id);
|
||||
} else {
|
||||
for (const id of expandedGroupIds.value) {
|
||||
if (availableExpanded.has(id)) nextExpanded.add(id);
|
||||
}
|
||||
}
|
||||
expandedGroupIds.value = nextExpanded;
|
||||
|
||||
if (selectedKeyRaw.value && !flatKeys.value.some((key) => key.key_raw === selectedKeyRaw.value)) {
|
||||
selectedKeyRaw.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadKeys() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await api.redisScanKeys(props.connectionId, props.db, 0, searchPattern.value, PAGE_SIZE);
|
||||
keys.value = result.keys;
|
||||
const result = await api.redisScanKeys(props.connectionId, props.db, 0, effectivePattern.value, PAGE_SIZE);
|
||||
flatKeys.value = result.keys;
|
||||
cursor.value = result.cursor;
|
||||
hasMore.value = result.cursor !== 0;
|
||||
selectedKey.value = null;
|
||||
selectedKeyRaw.value = null;
|
||||
rebuildTree(isSearchMode.value);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -45,25 +79,44 @@ async function loadMoreKeys() {
|
||||
|
||||
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))];
|
||||
const result = await api.redisScanKeys(
|
||||
props.connectionId,
|
||||
props.db,
|
||||
cursor.value,
|
||||
effectivePattern.value,
|
||||
PAGE_SIZE,
|
||||
);
|
||||
const existingKeys = new Set(flatKeys.value.map((key) => key.key_raw));
|
||||
flatKeys.value = [...flatKeys.value, ...result.keys.filter((key) => !existingKeys.has(key.key_raw))];
|
||||
cursor.value = result.cursor;
|
||||
hasMore.value = result.cursor !== 0;
|
||||
rebuildTree(isSearchMode.value);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function selectKey(key: string) {
|
||||
selectedKey.value = key;
|
||||
function toggleGroup(groupId: string) {
|
||||
const next = new Set(expandedGroupIds.value);
|
||||
if (next.has(groupId)) next.delete(groupId);
|
||||
else next.add(groupId);
|
||||
expandedGroupIds.value = next;
|
||||
}
|
||||
|
||||
function onRowClick(node: RedisKeyTreeNode) {
|
||||
if (node.kind === "group") {
|
||||
toggleGroup(node.id);
|
||||
return;
|
||||
}
|
||||
|
||||
selectedKeyRaw.value = node.keyRaw;
|
||||
}
|
||||
|
||||
function onKeyDeleted() {
|
||||
if (selectedKey.value) {
|
||||
keys.value = keys.value.filter((k) => k.key !== selectedKey.value);
|
||||
selectedKey.value = null;
|
||||
}
|
||||
if (!selectedKeyRaw.value) return;
|
||||
flatKeys.value = flatKeys.value.filter((key) => key.key_raw !== selectedKeyRaw.value);
|
||||
selectedKeyRaw.value = null;
|
||||
rebuildTree(false);
|
||||
}
|
||||
|
||||
function typeColor(type: string): string {
|
||||
@@ -110,33 +163,56 @@ onMounted(loadKeys);
|
||||
|
||||
<!-- Key count -->
|
||||
<div class="h-9 flex items-center px-3 text-xs text-muted-foreground border-b shrink-0">
|
||||
{{ loading && keys.length === 0 ? t("redis.loadingKeys") : t("redis.keys", { count: keys.length }) }}
|
||||
{{ loading && flatKeys.length === 0 ? t("redis.loadingKeys") : t("redis.keys", { count: flatKeys.length }) }}
|
||||
</div>
|
||||
|
||||
<!-- Key list -->
|
||||
<!-- Key tree -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div
|
||||
v-for="k in keys"
|
||||
:key="k.key"
|
||||
v-for="row in visibleRows"
|
||||
:key="row.node.id"
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-xs cursor-pointer hover:bg-accent/50 border-b border-border/50"
|
||||
:class="{ 'bg-accent': selectedKey === k.key }"
|
||||
@click="selectKey(k.key)"
|
||||
:class="{ 'bg-accent': row.node.kind === 'leaf' && selectedKeyRaw === row.node.keyRaw }"
|
||||
:style="{ paddingLeft: `${12 + row.depth * 18}px` }"
|
||||
:title="row.node.kind === 'leaf' ? row.node.fullKeyDisplay : row.node.pathSegments.join(':')"
|
||||
@click="onRowClick(row.node)"
|
||||
>
|
||||
<Key class="w-3 h-3 shrink-0" :class="typeColor(k.key_type)" />
|
||||
<span class="truncate flex-1 font-mono">{{ k.key }}</span>
|
||||
<Badge variant="outline" class="text-[10px] px-1 py-0 shrink-0">{{ k.key_type }}</Badge>
|
||||
<template v-if="row.node.kind === 'group'">
|
||||
<component
|
||||
:is="expandedGroupIds.has(row.node.id) ? ChevronDown : ChevronRight"
|
||||
class="w-3 h-3 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
<component
|
||||
:is="expandedGroupIds.has(row.node.id) ? FolderOpen : FolderClosed"
|
||||
class="w-3 h-3 shrink-0 text-amber-500"
|
||||
/>
|
||||
<span class="truncate flex-1 font-mono">{{ row.node.label }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="w-3 h-3 shrink-0" />
|
||||
<Key class="w-3 h-3 shrink-0" :class="typeColor(row.node.keyType)" />
|
||||
<span class="truncate font-mono">{{ row.node.label || row.node.fullKeyDisplay }}</span>
|
||||
<span
|
||||
v-if="row.node.label !== row.node.fullKeyDisplay"
|
||||
class="truncate flex-1 text-[10px] text-muted-foreground font-mono"
|
||||
>
|
||||
{{ row.node.fullKeyDisplay }}
|
||||
</span>
|
||||
<span v-else class="flex-1" />
|
||||
<Badge variant="outline" class="text-[10px] px-1 py-0 shrink-0">{{ row.node.keyType }}</Badge>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="keys.length === 0 && !loading" class="px-3 py-8 text-center text-muted-foreground text-xs">
|
||||
<div v-if="flatKeys.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"
|
||||
v-if="loading && flatKeys.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">
|
||||
<div v-if="hasMore || (loading && flatKeys.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") }}
|
||||
@@ -151,9 +227,11 @@ onMounted(loadKeys);
|
||||
<div class="h-full min-w-0">
|
||||
<RedisValueViewer
|
||||
v-if="selectedKey"
|
||||
:key="selectedKey"
|
||||
:key="selectedKey.key_raw"
|
||||
:connection-id="connectionId"
|
||||
:key-name="selectedKey"
|
||||
:db="db"
|
||||
:key-display="selectedKey.key_display"
|
||||
:key-raw="selectedKey.key_raw"
|
||||
@deleted="onKeyDeleted"
|
||||
/>
|
||||
<div v-else class="h-full flex items-center justify-center text-muted-foreground text-sm">
|
||||
|
||||
@@ -13,7 +13,9 @@ const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
connectionId: string;
|
||||
keyName: string;
|
||||
db: number;
|
||||
keyDisplay: string;
|
||||
keyRaw: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ deleted: [] }>();
|
||||
@@ -37,18 +39,20 @@ const pendingDelete = ref<PendingDelete | null>(null);
|
||||
const deleteDetails = computed(() => {
|
||||
const pending = pendingDelete.value;
|
||||
if (!pending) return "";
|
||||
if (pending.kind === "key") return t("dangerDialog.redisKeyDetails", { key: props.keyName });
|
||||
if (pending.kind === "key") return t("dangerDialog.redisKeyDetails", { key: props.keyDisplay });
|
||||
if (pending.kind === "hash")
|
||||
return t("dangerDialog.redisHashFieldDetails", { key: props.keyName, field: pending.field });
|
||||
return t("dangerDialog.redisHashFieldDetails", { key: props.keyDisplay, field: pending.field });
|
||||
if (pending.kind === "list")
|
||||
return t("dangerDialog.redisListItemDetails", { key: props.keyName, index: pending.index });
|
||||
return t("dangerDialog.redisSetMemberDetails", { key: props.keyName, member: pending.member });
|
||||
return t("dangerDialog.redisListItemDetails", { key: props.keyDisplay, index: pending.index });
|
||||
return t("dangerDialog.redisSetMemberDetails", { key: props.keyDisplay, member: pending.member });
|
||||
});
|
||||
|
||||
const isBinaryStringValue = computed(() => data.value?.key_type === "string" && data.value?.value_is_binary);
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
data.value = await api.redisGetValue(props.connectionId, props.keyName);
|
||||
data.value = await api.redisGetValue(props.connectionId, props.db, props.keyRaw);
|
||||
if (data.value.key_type === "string") {
|
||||
editValue.value = String(data.value.value);
|
||||
}
|
||||
@@ -58,13 +62,20 @@ async function load() {
|
||||
}
|
||||
|
||||
async function saveString() {
|
||||
await api.redisSetString(props.connectionId, props.keyName, editValue.value);
|
||||
if (isBinaryStringValue.value) return;
|
||||
await api.redisSetString(props.connectionId, props.db, props.keyRaw, editValue.value);
|
||||
isEditing.value = false;
|
||||
await load();
|
||||
}
|
||||
|
||||
function handleStringInput() {
|
||||
if (!isBinaryStringValue.value) {
|
||||
isEditing.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyDeleteKey() {
|
||||
await api.redisDeleteKey(props.connectionId, props.keyName);
|
||||
await api.redisDeleteKey(props.connectionId, props.db, props.keyRaw);
|
||||
emit("deleted");
|
||||
}
|
||||
|
||||
@@ -82,13 +93,13 @@ function copyValue() {
|
||||
// Hash
|
||||
async function hashSet() {
|
||||
if (!newField.value) return;
|
||||
await api.redisHashSet(props.connectionId, props.keyName, newField.value, newValue.value);
|
||||
await api.redisHashSet(props.connectionId, props.db, props.keyRaw, newField.value, newValue.value);
|
||||
newField.value = "";
|
||||
newValue.value = "";
|
||||
await load();
|
||||
}
|
||||
async function applyHashDel(field: string) {
|
||||
await api.redisHashDel(props.connectionId, props.keyName, field);
|
||||
await api.redisHashDel(props.connectionId, props.db, props.keyRaw, field);
|
||||
await load();
|
||||
}
|
||||
function requestHashDel(field: string) {
|
||||
@@ -99,12 +110,12 @@ function requestHashDel(field: string) {
|
||||
// List
|
||||
async function listPush() {
|
||||
if (!newValue.value) return;
|
||||
await api.redisListPush(props.connectionId, props.keyName, newValue.value);
|
||||
await api.redisListPush(props.connectionId, props.db, props.keyRaw, newValue.value);
|
||||
newValue.value = "";
|
||||
await load();
|
||||
}
|
||||
async function applyListRemove(index: number) {
|
||||
await api.redisListRemove(props.connectionId, props.keyName, index);
|
||||
await api.redisListRemove(props.connectionId, props.db, props.keyRaw, index);
|
||||
await load();
|
||||
}
|
||||
function requestListRemove(index: number) {
|
||||
@@ -115,12 +126,12 @@ function requestListRemove(index: number) {
|
||||
// Set
|
||||
async function setAdd() {
|
||||
if (!newValue.value) return;
|
||||
await api.redisSetAdd(props.connectionId, props.keyName, newValue.value);
|
||||
await api.redisSetAdd(props.connectionId, props.db, props.keyRaw, newValue.value);
|
||||
newValue.value = "";
|
||||
await load();
|
||||
}
|
||||
async function applySetRemove(member: string) {
|
||||
await api.redisSetRemove(props.connectionId, props.keyName, member);
|
||||
await api.redisSetRemove(props.connectionId, props.db, props.keyRaw, member);
|
||||
await load();
|
||||
}
|
||||
function requestSetRemove(member: string) {
|
||||
@@ -155,7 +166,7 @@ onMounted(load);
|
||||
<template v-else-if="data">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-2 px-4 py-2 border-b bg-muted/30 shrink-0">
|
||||
<span class="font-mono text-sm font-medium truncate">{{ data.key }}</span>
|
||||
<span class="font-mono text-sm font-medium truncate">{{ data.key_display }}</span>
|
||||
<Badge variant="secondary" class="text-xs">{{ data.key_type }}</Badge>
|
||||
<Badge v-if="data.ttl > 0" variant="outline" class="text-xs">TTL: {{ data.ttl }}s</Badge>
|
||||
<Badge v-else-if="data.ttl === -1" variant="outline" class="text-xs opacity-50">{{
|
||||
@@ -174,8 +185,12 @@ onMounted(load);
|
||||
<textarea
|
||||
v-model="editValue"
|
||||
class="flex-1 p-4 font-mono text-sm bg-background resize-none outline-none"
|
||||
@input="isEditing = true"
|
||||
:readonly="isBinaryStringValue"
|
||||
@input="handleStringInput"
|
||||
/>
|
||||
<div v-if="isBinaryStringValue" class="px-4 py-2 border-t text-xs text-muted-foreground shrink-0">
|
||||
{{ t("redis.binaryStringReadonlyHint") }}
|
||||
</div>
|
||||
<div v-if="isEditing" class="px-4 py-2 border-t flex justify-end gap-2 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -532,6 +532,8 @@ export default {
|
||||
members: "{count} members",
|
||||
entries: "{count} entries",
|
||||
noExpiry: "no expiry",
|
||||
binaryStringReadonlyHint:
|
||||
"Binary string values are shown as escaped text in read-only mode; editing raw bytes is not supported.",
|
||||
},
|
||||
mongo: {
|
||||
documents: "{count} documents",
|
||||
|
||||
@@ -523,6 +523,7 @@ export default {
|
||||
members: "{count} 个成员",
|
||||
entries: "{count} 条记录",
|
||||
noExpiry: "永不过期",
|
||||
binaryStringReadonlyHint: "二进制字符串按转义文本只读展示;当前不支持直接编辑原始字节值。",
|
||||
},
|
||||
mongo: {
|
||||
documents: "{count} 个文档",
|
||||
|
||||
+30
-18
@@ -450,40 +450,52 @@ export async function redisScanKeys(
|
||||
return post("/api/redis/scan-keys", { connectionId, db, cursor, pattern, count });
|
||||
}
|
||||
|
||||
export async function redisGetValue(connectionId: string, key: string): Promise<RedisValue> {
|
||||
return post("/api/redis/get-value", { connectionId, key });
|
||||
export async function redisGetValue(connectionId: string, db: number, keyRaw: string): Promise<RedisValue> {
|
||||
return post("/api/redis/get-value", { connectionId, db, keyRaw });
|
||||
}
|
||||
|
||||
export async function redisSetString(connectionId: string, key: string, value: string, ttl?: number): Promise<void> {
|
||||
return post("/api/redis/set-string", { connectionId, key, value, ttl });
|
||||
export async function redisSetString(
|
||||
connectionId: string,
|
||||
db: number,
|
||||
keyRaw: string,
|
||||
value: string,
|
||||
ttl?: number,
|
||||
): Promise<void> {
|
||||
return post("/api/redis/set-string", { connectionId, db, keyRaw, value, ttl });
|
||||
}
|
||||
|
||||
export async function redisDeleteKey(connectionId: string, key: string): Promise<void> {
|
||||
return post("/api/redis/delete-key", { connectionId, key });
|
||||
export async function redisDeleteKey(connectionId: string, db: number, keyRaw: string): Promise<void> {
|
||||
return post("/api/redis/delete-key", { connectionId, db, keyRaw });
|
||||
}
|
||||
|
||||
export async function redisHashSet(connectionId: string, key: string, field: string, value: string): Promise<void> {
|
||||
return post("/api/redis/hash-set", { connectionId, key, field, value });
|
||||
export async function redisHashSet(
|
||||
connectionId: string,
|
||||
db: number,
|
||||
keyRaw: string,
|
||||
field: string,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
return post("/api/redis/hash-set", { connectionId, db, keyRaw, field, value });
|
||||
}
|
||||
|
||||
export async function redisHashDel(connectionId: string, key: string, field: string): Promise<void> {
|
||||
return post("/api/redis/hash-del", { connectionId, key, field });
|
||||
export async function redisHashDel(connectionId: string, db: number, keyRaw: string, field: string): Promise<void> {
|
||||
return post("/api/redis/hash-del", { connectionId, db, keyRaw, field });
|
||||
}
|
||||
|
||||
export async function redisListPush(connectionId: string, key: string, value: string): Promise<void> {
|
||||
return post("/api/redis/list-push", { connectionId, key, value });
|
||||
export async function redisListPush(connectionId: string, db: number, keyRaw: string, value: string): Promise<void> {
|
||||
return post("/api/redis/list-push", { connectionId, db, keyRaw, value });
|
||||
}
|
||||
|
||||
export async function redisListRemove(connectionId: string, key: string, index: number): Promise<void> {
|
||||
return post("/api/redis/list-remove", { connectionId, key, index });
|
||||
export async function redisListRemove(connectionId: string, db: number, keyRaw: string, index: number): Promise<void> {
|
||||
return post("/api/redis/list-remove", { connectionId, db, keyRaw, index });
|
||||
}
|
||||
|
||||
export async function redisSetAdd(connectionId: string, key: string, member: string): Promise<void> {
|
||||
return post("/api/redis/set-add", { connectionId, key, member });
|
||||
export async function redisSetAdd(connectionId: string, db: number, keyRaw: string, member: string): Promise<void> {
|
||||
return post("/api/redis/set-add", { connectionId, db, keyRaw, member });
|
||||
}
|
||||
|
||||
export async function redisSetRemove(connectionId: string, key: string, member: string): Promise<void> {
|
||||
return post("/api/redis/set-remove", { connectionId, key, member });
|
||||
export async function redisSetRemove(connectionId: string, db: number, keyRaw: string, member: string): Promise<void> {
|
||||
return post("/api/redis/set-remove", { connectionId, db, keyRaw, member });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import type { RedisKeyInfo } from "./api";
|
||||
|
||||
export interface RedisKeyTreeLeafNode {
|
||||
kind: "leaf";
|
||||
id: string;
|
||||
label: string;
|
||||
fullKeyDisplay: string;
|
||||
keyRaw: string;
|
||||
db: number;
|
||||
keyType: string;
|
||||
ttl: number;
|
||||
pathSegments: string[];
|
||||
}
|
||||
|
||||
export interface RedisKeyTreeGroupNode {
|
||||
kind: "group";
|
||||
id: string;
|
||||
label: string;
|
||||
pathSegments: string[];
|
||||
children: RedisKeyTreeNode[];
|
||||
}
|
||||
|
||||
export type RedisKeyTreeNode = RedisKeyTreeLeafNode | RedisKeyTreeGroupNode;
|
||||
|
||||
export interface RedisKeyTreeRow {
|
||||
node: RedisKeyTreeNode;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
function buildGroupId(db: number, pathSegments: string[]): string {
|
||||
return `group:${db}:${pathSegments.join("\u0000")}`;
|
||||
}
|
||||
|
||||
function buildLeafId(db: number, keyRaw: string): string {
|
||||
return `leaf:${db}:${keyRaw}`;
|
||||
}
|
||||
|
||||
function compareRedisTreeNodes(a: RedisKeyTreeNode, b: RedisKeyTreeNode): number {
|
||||
if (a.kind !== b.kind) return a.kind === "group" ? -1 : 1;
|
||||
return a.label.localeCompare(b.label);
|
||||
}
|
||||
|
||||
function sortRedisTreeNodes(nodes: RedisKeyTreeNode[]): RedisKeyTreeNode[] {
|
||||
return [...nodes].sort(compareRedisTreeNodes).map((node) =>
|
||||
node.kind === "group"
|
||||
? {
|
||||
...node,
|
||||
children: sortRedisTreeNodes(node.children),
|
||||
}
|
||||
: node,
|
||||
);
|
||||
}
|
||||
|
||||
export function buildRedisKeyTree(keys: RedisKeyInfo[], db: number): RedisKeyTreeNode[] {
|
||||
const root: RedisKeyTreeNode[] = [];
|
||||
const groupMap = new Map<string, RedisKeyTreeGroupNode>();
|
||||
|
||||
for (const key of keys) {
|
||||
const pathSegments = key.key_display.split(":");
|
||||
if (pathSegments.length === 1) {
|
||||
root.push({
|
||||
kind: "leaf",
|
||||
id: buildLeafId(db, key.key_raw),
|
||||
label: pathSegments[0],
|
||||
fullKeyDisplay: key.key_display,
|
||||
keyRaw: key.key_raw,
|
||||
db,
|
||||
keyType: key.key_type,
|
||||
ttl: key.ttl,
|
||||
pathSegments,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let currentLevel = root;
|
||||
const groupSegments: string[] = [];
|
||||
for (const segment of pathSegments.slice(0, -1)) {
|
||||
groupSegments.push(segment);
|
||||
const groupId = buildGroupId(db, groupSegments);
|
||||
let group = groupMap.get(groupId);
|
||||
if (!group) {
|
||||
group = {
|
||||
kind: "group",
|
||||
id: groupId,
|
||||
label: segment,
|
||||
pathSegments: [...groupSegments],
|
||||
children: [],
|
||||
};
|
||||
groupMap.set(groupId, group);
|
||||
currentLevel.push(group);
|
||||
}
|
||||
currentLevel = group.children;
|
||||
}
|
||||
|
||||
currentLevel.push({
|
||||
kind: "leaf",
|
||||
id: buildLeafId(db, key.key_raw),
|
||||
label: pathSegments[pathSegments.length - 1],
|
||||
fullKeyDisplay: key.key_display,
|
||||
keyRaw: key.key_raw,
|
||||
db,
|
||||
keyType: key.key_type,
|
||||
ttl: key.ttl,
|
||||
pathSegments,
|
||||
});
|
||||
}
|
||||
|
||||
return sortRedisTreeNodes(root);
|
||||
}
|
||||
|
||||
export function collectExpandedGroupIds(nodes: RedisKeyTreeNode[]): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
|
||||
const visit = (entries: RedisKeyTreeNode[]) => {
|
||||
for (const node of entries) {
|
||||
if (node.kind !== "group") continue;
|
||||
ids.add(node.id);
|
||||
visit(node.children);
|
||||
}
|
||||
};
|
||||
|
||||
visit(nodes);
|
||||
return ids;
|
||||
}
|
||||
|
||||
export function flattenVisibleRedisKeyTree(
|
||||
nodes: RedisKeyTreeNode[],
|
||||
expandedGroupIds: ReadonlySet<string>,
|
||||
depth = 0,
|
||||
): RedisKeyTreeRow[] {
|
||||
const rows: RedisKeyTreeRow[] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
rows.push({ node, depth });
|
||||
if (node.kind === "group" && expandedGroupIds.has(node.id)) {
|
||||
rows.push(...flattenVisibleRedisKeyTree(node.children, expandedGroupIds, depth + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
+35
-20
@@ -258,15 +258,18 @@ export async function getAppVersion(): Promise<string> {
|
||||
|
||||
// --- Redis ---
|
||||
export interface RedisKeyInfo {
|
||||
key: string;
|
||||
key_display: string;
|
||||
key_raw: string;
|
||||
key_type: string;
|
||||
ttl: number;
|
||||
}
|
||||
|
||||
export interface RedisValue {
|
||||
key: string;
|
||||
key_display: string;
|
||||
key_raw: string;
|
||||
key_type: string;
|
||||
ttl: number;
|
||||
value_is_binary: boolean;
|
||||
value: any;
|
||||
}
|
||||
|
||||
@@ -289,40 +292,52 @@ export async function redisScanKeys(
|
||||
return invoke("redis_scan_keys", { connectionId, db, cursor, pattern, count });
|
||||
}
|
||||
|
||||
export async function redisGetValue(connectionId: string, key: string): Promise<RedisValue> {
|
||||
return invoke("redis_get_value", { connectionId, key });
|
||||
export async function redisGetValue(connectionId: string, db: number, keyRaw: string): Promise<RedisValue> {
|
||||
return invoke("redis_get_value", { connectionId, db, keyRaw });
|
||||
}
|
||||
|
||||
export async function redisSetString(connectionId: string, key: string, value: string, ttl?: number): Promise<void> {
|
||||
return invoke("redis_set_string", { connectionId, key, value, ttl });
|
||||
export async function redisSetString(
|
||||
connectionId: string,
|
||||
db: number,
|
||||
keyRaw: string,
|
||||
value: string,
|
||||
ttl?: number,
|
||||
): Promise<void> {
|
||||
return invoke("redis_set_string", { connectionId, db, keyRaw, value, ttl });
|
||||
}
|
||||
|
||||
export async function redisDeleteKey(connectionId: string, key: string): Promise<void> {
|
||||
return invoke("redis_delete_key", { connectionId, key });
|
||||
export async function redisDeleteKey(connectionId: string, db: number, keyRaw: string): Promise<void> {
|
||||
return invoke("redis_delete_key", { connectionId, db, keyRaw });
|
||||
}
|
||||
|
||||
export async function redisHashSet(connectionId: string, key: string, field: string, value: string): Promise<void> {
|
||||
return invoke("redis_hash_set", { connectionId, key, field, value });
|
||||
export async function redisHashSet(
|
||||
connectionId: string,
|
||||
db: number,
|
||||
keyRaw: string,
|
||||
field: string,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
return invoke("redis_hash_set", { connectionId, db, keyRaw, field, value });
|
||||
}
|
||||
|
||||
export async function redisHashDel(connectionId: string, key: string, field: string): Promise<void> {
|
||||
return invoke("redis_hash_del", { connectionId, key, field });
|
||||
export async function redisHashDel(connectionId: string, db: number, keyRaw: string, field: string): Promise<void> {
|
||||
return invoke("redis_hash_del", { connectionId, db, keyRaw, field });
|
||||
}
|
||||
|
||||
export async function redisListPush(connectionId: string, key: string, value: string): Promise<void> {
|
||||
return invoke("redis_list_push", { connectionId, key, value });
|
||||
export async function redisListPush(connectionId: string, db: number, keyRaw: string, value: string): Promise<void> {
|
||||
return invoke("redis_list_push", { connectionId, db, keyRaw, value });
|
||||
}
|
||||
|
||||
export async function redisListRemove(connectionId: string, key: string, index: number): Promise<void> {
|
||||
return invoke("redis_list_remove", { connectionId, key, index });
|
||||
export async function redisListRemove(connectionId: string, db: number, keyRaw: string, index: number): Promise<void> {
|
||||
return invoke("redis_list_remove", { connectionId, db, keyRaw, index });
|
||||
}
|
||||
|
||||
export async function redisSetAdd(connectionId: string, key: string, member: string): Promise<void> {
|
||||
return invoke("redis_set_add", { connectionId, key, member });
|
||||
export async function redisSetAdd(connectionId: string, db: number, keyRaw: string, member: string): Promise<void> {
|
||||
return invoke("redis_set_add", { connectionId, db, keyRaw, member });
|
||||
}
|
||||
|
||||
export async function redisSetRemove(connectionId: string, key: string, member: string): Promise<void> {
|
||||
return invoke("redis_set_remove", { connectionId, key, member });
|
||||
export async function redisSetRemove(connectionId: string, db: number, keyRaw: string, member: string): Promise<void> {
|
||||
return invoke("redis_set_remove", { connectionId, db, keyRaw, member });
|
||||
}
|
||||
|
||||
// --- MongoDB ---
|
||||
|
||||
Reference in New Issue
Block a user