fix(redis): 修复 Redis set 类型的值中有乱码 (#2554)
This commit is contained in:
parent
842dcc6cab
commit
f48e751d90
|
|
@ -213,11 +213,12 @@ function escapeHtml(value: string): string {
|
|||
const deleteDetails = computed(() => {
|
||||
const pending = pendingDelete.value;
|
||||
if (!pending) return "";
|
||||
if (pending.kind === "key") return t("dangerDialog.redisKeyDetails", { key: props.keyDisplay });
|
||||
if (pending.kind === "hash") return t("dangerDialog.redisHashFieldDetails", { key: props.keyDisplay, field: pending.field });
|
||||
if (pending.kind === "list") return t("dangerDialog.redisListItemDetails", { key: props.keyDisplay, index: pending.index });
|
||||
if (pending.kind === "zset") return t("dangerDialog.redisSetMemberDetails", { key: props.keyDisplay, member: pending.member });
|
||||
return t("dangerDialog.redisSetMemberDetails", { key: props.keyDisplay, member: pending.member });
|
||||
const key = formatValue(props.keyDisplay);
|
||||
if (pending.kind === "key") return t("dangerDialog.redisKeyDetails", { key });
|
||||
if (pending.kind === "hash") return t("dangerDialog.redisHashFieldDetails", { key, field: formatValue(pending.field) });
|
||||
if (pending.kind === "list") return t("dangerDialog.redisListItemDetails", { key, index: pending.index });
|
||||
if (pending.kind === "zset") return t("dangerDialog.redisSetMemberDetails", { key, member: formatValue(pending.member) });
|
||||
return t("dangerDialog.redisSetMemberDetails", { key, member: formatValue(pending.member) });
|
||||
});
|
||||
|
||||
const isBinaryStringValue = computed(() => data.value?.key_type === "string" && data.value?.value_is_binary);
|
||||
|
|
@ -465,7 +466,7 @@ async function copyInsertStatement() {
|
|||
}
|
||||
|
||||
function copyMember(value: unknown) {
|
||||
void copyText(formatRedisMemberDetail(value).text);
|
||||
void copyText(formatRedisMemberDetail(value).rawText);
|
||||
}
|
||||
|
||||
function selectMember(title: string, value: unknown, context: RedisMemberContext) {
|
||||
|
|
@ -581,12 +582,12 @@ function startResizeZsetColumns(event: PointerEvent) {
|
|||
}
|
||||
|
||||
function startEditMember() {
|
||||
memberEditValue.value = selectedMemberDetail.value.text;
|
||||
memberEditValue.value = selectedMemberDetail.value.rawText;
|
||||
isEditingMember.value = true;
|
||||
}
|
||||
|
||||
function cancelEditMember() {
|
||||
memberEditValue.value = selectedMemberDetail.value.text;
|
||||
memberEditValue.value = selectedMemberDetail.value.rawText;
|
||||
isEditingMember.value = false;
|
||||
}
|
||||
|
||||
|
|
@ -815,7 +816,7 @@ onBeforeUnmount(() => {
|
|||
<!-- Header -->
|
||||
<div class="shrink-0 border-b bg-background">
|
||||
<div class="flex h-9 items-center gap-2 px-4">
|
||||
<span class="dbx-editor-font-family min-w-0 flex-1 truncate text-sm font-semibold">{{ data.key_display }}</span>
|
||||
<span class="dbx-editor-font-family min-w-0 flex-1 truncate text-sm font-semibold">{{ formatValue(data.key_display) }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0" @click="load"><RefreshCw class="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0" @click="copyValue"><Copy class="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0" :title="t('redis.copyInsertStatement')" @click="copyInsertStatement"><Terminal class="h-3.5 w-3.5" /></Button>
|
||||
|
|
@ -906,7 +907,7 @@ onBeforeUnmount(() => {
|
|||
@click="viewMember(`#${row.index}`, row.value, { kind: 'list', index: row.index })"
|
||||
>
|
||||
<div class="px-3 py-1.5 text-xs text-muted-foreground border-r">{{ row.index }}</div>
|
||||
<div class="px-3 py-1.5 truncate">{{ row.value }}</div>
|
||||
<div class="px-3 py-1.5 truncate">{{ formatValue(row.value) }}</div>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100" :title="t('redis.viewMember')" @click.stop="viewMember(`#${row.index}`, row.value, { kind: 'list', index: row.index })"><Eye class="w-3 h-3" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100" :title="t('redis.copyMember')" @click.stop="copyMember(row.value)"><Copy class="w-3 h-3" /></Button>
|
||||
|
|
@ -946,7 +947,7 @@ onBeforeUnmount(() => {
|
|||
:style="{ height: `${REDIS_COLLECTION_ROW_HEIGHT}px` }"
|
||||
@click="viewMember(t('redis.member'), row.value, { kind: 'set', member: String(row.value) })"
|
||||
>
|
||||
<div class="px-3 py-1.5 truncate">{{ row.value }}</div>
|
||||
<div class="px-3 py-1.5 truncate">{{ formatValue(row.value) }}</div>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100" :title="t('redis.viewMember')" @click.stop="viewMember(t('redis.member'), row.value, { kind: 'set', member: String(row.value) })"><Eye class="w-3 h-3" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100" :title="t('redis.copyMember')" @click.stop="copyMember(row.value)"><Copy class="w-3 h-3" /></Button>
|
||||
|
|
@ -1004,8 +1005,8 @@ onBeforeUnmount(() => {
|
|||
:class="{ 'bg-accent/60': isSelectedMember(String(row.value.field), row.value.value) }"
|
||||
@click="viewMember(String(row.value.field), row.value.value, { kind: 'hash', field: String(row.value.field) })"
|
||||
>
|
||||
<div class="px-3 py-1.5 text-blue-500 truncate border-r">{{ row.value.field }}</div>
|
||||
<div class="px-3 py-1.5 truncate text-muted-foreground">{{ row.value.value }}</div>
|
||||
<div class="px-3 py-1.5 text-blue-500 truncate border-r">{{ formatValue(row.value.field) }}</div>
|
||||
<div class="px-3 py-1.5 truncate text-muted-foreground">{{ formatValue(row.value.value) }}</div>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
@ -1071,8 +1072,8 @@ onBeforeUnmount(() => {
|
|||
<div class="px-3 py-1.5 text-muted-foreground text-xs border-r min-w-0 truncate" :title="String(row.value.score)">
|
||||
{{ row.value.score }}
|
||||
</div>
|
||||
<div class="px-3 py-1.5 min-w-0 truncate" :title="String(row.value.member)">
|
||||
{{ row.value.member }}
|
||||
<div class="px-3 py-1.5 min-w-0 truncate" :title="formatValue(row.value.member)">
|
||||
{{ formatValue(row.value.member) }}
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
|
|
@ -1156,7 +1157,7 @@ onBeforeUnmount(() => {
|
|||
<div class="absolute inset-y-0 left-0 z-10 w-2 -translate-x-1 cursor-col-resize border-l border-transparent hover:border-primary/60" @pointerdown.prevent="startResizeMemberSheet" />
|
||||
<SheetHeader class="border-b px-5 py-4 pr-12">
|
||||
<SheetTitle class="flex items-center gap-2">
|
||||
<span class="truncate">{{ selectedMemberTitle || t("redis.memberDetail") }}</span>
|
||||
<span class="truncate">{{ selectedMemberTitle ? formatValue(selectedMemberTitle) : t("redis.memberDetail") }}</span>
|
||||
<Badge variant="outline" class="shrink-0 text-xs">{{ selectedMemberDetail.format.toUpperCase() }}</Badge>
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
|
@ -1218,7 +1219,7 @@ onBeforeUnmount(() => {
|
|||
<Pencil class="h-4 w-4" />
|
||||
{{ t("redis.editMember") }}
|
||||
</Button>
|
||||
<Button variant="outline" @click="copyText(selectedMemberDetail.text)">
|
||||
<Button variant="outline" @click="copyText(selectedMemberDetail.rawText)">
|
||||
<Copy class="h-4 w-4" />
|
||||
{{ t("redis.copyMember") }}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { formatRedisMemberDetail, formatRedisStringValue, getRedisMemberSelectionKey, sanitizeRedisDisplayText } from "@/lib/redis/redisValuePresentation";
|
||||
|
||||
describe("redisValuePresentation", () => {
|
||||
it("strips control bytes from display without mutating raw member text", () => {
|
||||
const raw = "send_message_to_esb\x06\x16\x06\x16send_message_to_esb";
|
||||
|
||||
const detail = formatRedisMemberDetail(raw);
|
||||
|
||||
expect(detail.text).toBe("send_message_to_esbsend_message_to_esb");
|
||||
expect(detail.rawText).toBe(raw);
|
||||
});
|
||||
|
||||
it("preserves common whitespace in display text", () => {
|
||||
expect(sanitizeRedisDisplayText("line1\nline2\tvalue\r\n")).toBe("line1\nline2\tvalue\r\n");
|
||||
});
|
||||
|
||||
it("strips utf8 c1 control bytes for display", () => {
|
||||
expect(sanitizeRedisDisplayText("before\u0085after")).toBe("beforeafter");
|
||||
});
|
||||
|
||||
it("uses raw member text for selection keys", () => {
|
||||
const raw = "send_message_to_esb\x06\x16";
|
||||
|
||||
expect(getRedisMemberSelectionKey("member", raw)).toBe(`member\n${raw}`);
|
||||
});
|
||||
|
||||
it("formats string values for display without changing plain text", () => {
|
||||
expect(formatRedisStringValue("plain-text")).toBe("plain-text");
|
||||
});
|
||||
});
|
||||
|
|
@ -30,7 +30,7 @@ export function clampRedisMemberDetailSheetWidth(width: number, viewportWidth: n
|
|||
export function formatRedisMemberDetail(value: unknown): RedisMemberDetail {
|
||||
if (typeof value === "string") {
|
||||
const json = parseRedisJsonDetail(value);
|
||||
return json ? { text: json.formattedText, rawText: value, format: "json", json } : { text: value, rawText: value, format: "text" };
|
||||
return json ? { text: json.formattedText, rawText: value, format: "json", json } : { text: sanitizeRedisDisplayText(value), rawText: value, format: "text" };
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -49,7 +49,7 @@ export function formatRedisMemberDetail(value: unknown): RedisMemberDetail {
|
|||
|
||||
export function formatRedisStringValue(value: unknown): string {
|
||||
if (typeof value !== "string") return String(value ?? "");
|
||||
return formatRedisJsonString(value) ?? value;
|
||||
return formatRedisJsonString(value) ?? sanitizeRedisDisplayText(value);
|
||||
}
|
||||
|
||||
export function formatRedisCommandResult(value: unknown): string {
|
||||
|
|
@ -117,7 +117,26 @@ function isJsonContainer(value: unknown): boolean {
|
|||
}
|
||||
|
||||
export function getRedisMemberSelectionKey(title: string, value: unknown): string {
|
||||
return `${title}\n${formatRedisMemberDetail(value).text}`;
|
||||
const detail = formatRedisMemberDetail(value);
|
||||
return `${title}\n${detail.format === "json" ? detail.text : detail.rawText}`;
|
||||
}
|
||||
|
||||
export function sanitizeRedisDisplayText(value: string): string {
|
||||
let output = "";
|
||||
for (const ch of value) {
|
||||
if (ch === "\n" || ch === "\r" || ch === "\t") {
|
||||
output += ch;
|
||||
continue;
|
||||
}
|
||||
if (ch >= " " && ch !== "\u007f" && !isUtf8ControlCharacter(ch)) {
|
||||
output += ch;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function isUtf8ControlCharacter(ch: string): boolean {
|
||||
return /\p{Cc}/u.test(ch);
|
||||
}
|
||||
|
||||
export function highlightRedisJsonDetail(json: string): string {
|
||||
|
|
|
|||
Loading…
Reference in New Issue