From 83651c46d6cdb6fc50c754145169758acfc23212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=88=E7=A7=8B?= <49514152+yiqiui@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:21:59 +0800 Subject: [PATCH] feat(redis): auto-format detected JSON values --- .../src/components/redis/RedisValueViewer.vue | 77 +++++++++-- apps/desktop/src/i18n/locales/az.ts | 5 + apps/desktop/src/i18n/locales/en.ts | 5 + apps/desktop/src/i18n/locales/es.ts | 5 + apps/desktop/src/i18n/locales/it.ts | 5 + apps/desktop/src/i18n/locales/ja.ts | 5 + apps/desktop/src/i18n/locales/ko.ts | 5 + apps/desktop/src/i18n/locales/pt-BR.ts | 5 + apps/desktop/src/i18n/locales/ru.ts | 5 + apps/desktop/src/i18n/locales/tr.ts | 5 + apps/desktop/src/i18n/locales/zh-CN.ts | 5 + apps/desktop/src/i18n/locales/zh-TW.ts | 5 + .../RedisValueViewer.valueDetection.spec.ts | 124 ++++++++++++++++++ .../redis/redisValuePresentation.spec.ts | 66 ++++++++++ .../src/lib/redis/redisValuePresentation.ts | 34 +++++ 15 files changed, 343 insertions(+), 13 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/redis/RedisValueViewer.valueDetection.spec.ts diff --git a/apps/desktop/src/components/redis/RedisValueViewer.vue b/apps/desktop/src/components/redis/RedisValueViewer.vue index c1d2cb29dd..9ffa7f67ea 100644 --- a/apps/desktop/src/components/redis/RedisValueViewer.vue +++ b/apps/desktop/src/components/redis/RedisValueViewer.vue @@ -28,14 +28,16 @@ import { copyToClipboard } from "@/lib/common/clipboard"; import { formatTtl } from "@/lib/common/ttlFormat"; import { computeDisplayTtl, computeTtlCountdownTick, computeTtlCountdownValue, computeTtlForExpiryEdit, DEFAULT_REDIS_AUTO_REFRESH_INTERVAL_SECONDS, normalizeRedisAutoRefreshInterval } from "@/lib/redis/redisAutoRefresh"; import { + autoRedisValueFormat, canRenderRedisValueFormat, canEditRedisMemberDetail, decodeRedisBlob, + detectedRedisStructuredCodec, formatRedisMemberDetail, getRedisMemberSelectionKey, isRedisBlob, + isRedisJsonContainerValue, parseRedisJsonDetail, - preferredRedisValueFormat, REDIS_VALUE_FORMAT_DISPLAY_ORDER, redisBlobText, redisCollectionPageItems, @@ -57,6 +59,7 @@ import { REDIS_VALUE_CODEC_ORDER, type RedisCollectionItem, type RedisHashRowCopyTarget, + type RedisMemberDetail, type RedisValueCodec, type RedisValueFormat, } from "@/lib/redis/redisValuePresentation"; @@ -193,10 +196,10 @@ const isResizingHashColumns = ref(false); const zsetTableRef = ref(null); const zsetScoreWidth = ref(220); const isResizingZsetColumns = ref(false); -const stringValueView = ref(readPreferredRedisValueFormat()); -const memberValueView = ref(readPreferredRedisValueFormat()); -const stringValueCodec = ref(readPreferredRedisValueCodec()); -const memberValueCodec = ref(readPreferredRedisValueCodec()); +const stringValueView = ref(readStoredRedisValueFormat() ?? "utf8"); +const memberValueView = ref(readStoredRedisValueFormat() ?? "utf8"); +const stringValueCodec = ref(readStoredRedisValueCodec() ?? "none"); +const memberValueCodec = ref(readStoredRedisValueCodec() ?? "none"); const redisJsonWordWrap = ref(readRedisJsonWordWrap()); const redisJsonUnicodeMode = ref(readRedisJsonUnicodeMode()); const redisJsonDecoded = computed(() => redisJsonUnicodeMode.value === "decoded"); @@ -479,6 +482,40 @@ watch([memberValueCodec, selectedMemberRaw], ([codec]) => { const stringGzipBadge = computed(() => (stringBlob.value ? isGzipMagic(decodeRedisBlob(stringBlob.value)) : false)); const memberGzipBadge = computed(() => (isRedisBlob(selectedMemberRaw.value) ? isGzipMagic(decodeRedisBlob(selectedMemberRaw.value)) : false)); +/** + * Badge label for the detected payload representation (issue #10922): JSON + * containers and deterministic binary formats the user would otherwise see + * as an opaque blob. Plain strings and scalar JSON get no badge. + */ +function redisDetectionBadgeLabel(detail: RedisMemberDetail | null): string | null { + if (!detail) return null; + if (detail.javaSerialized) return t("redis.detectedJavaSerialized"); + if (detail.pickle) return t("redis.detectedPickle"); + if (detail.msgpack) return t("redis.detectedMsgpack"); + if (detail.phpSerialized) return t("redis.detectedPhpSerialized"); + if (isRedisJsonContainerValue(detail.json?.value)) return "JSON"; + return null; +} + +const stringValueDetectionLabel = computed(() => redisDetectionBadgeLabel(stringValueDetail.value)); +const memberDetectionLabel = computed(() => redisDetectionBadgeLabel(selectedMemberDetail.value)); + +/** + * Issue #10922: payloads with a deterministic container format open decoded + * instead of as an opaque blob — but only while the user has not pinned a + * codec; any explicit codec choice (including "none") disables detection. + */ +function applyDetectedStringCodec(detail: RedisMemberDetail) { + if (isStringValueTruncated.value) return; + if (readStoredRedisValueCodec() != null) return; + stringValueCodec.value = detectedRedisStructuredCodec(detail) ?? "none"; +} + +function applyDetectedMemberCodec(detail: RedisMemberDetail) { + if (readStoredRedisValueCodec() != null) return; + memberValueCodec.value = detectedRedisStructuredCodec(detail) ?? "none"; +} + interface RedisDecodedDetail { formattedText: string; value: unknown; @@ -1275,22 +1312,27 @@ function rememberRedisJsonUnicodeMode(mode: RedisJsonUnicodeMode) { } } -function readPreferredRedisValueFormat(): RedisValueFormat { +/** + * The persisted format preference, or null when the user never pinned one. + * Null is the auto-detection signal for issue #10922: detected JSON opens in + * the JSON view instead of raw UTF-8. Legacy values ("raw") map like before. + */ +function readStoredRedisValueFormat(): RedisValueFormat | null { try { const stored = localStorage.getItem(REDIS_VALUE_FORMAT_STORAGE_KEY); if (stored === "raw") return "utf8"; - return stored === "utf8" || stored === "ascii" || stored === "binary" || stored === "json" || stored === "hex" || stored === "base64" ? stored : "utf8"; + return stored === "utf8" || stored === "ascii" || stored === "binary" || stored === "json" || stored === "hex" || stored === "base64" ? stored : null; } catch { - return "utf8"; + return null; } } -function readPreferredRedisValueCodec(): RedisValueCodec { +function readStoredRedisValueCodec(): RedisValueCodec | null { try { const stored = localStorage.getItem(REDIS_VALUE_CODEC_STORAGE_KEY); - return isRedisValueCodec(stored) ? stored : "none"; + return isRedisValueCodec(stored) ? stored : null; } catch { - return "none"; + return null; } } @@ -1634,7 +1676,13 @@ async function load(options: { background?: boolean; notifyParent?: boolean; pre const allowJsonText = loadedValue.data.kind === "string"; if (loadedValue.data.truncated) stringValueCodec.value = "none"; const detail = formatRedisMemberDetail(loadedValue.data.content, { allowJsonText }); - stringValueView.value = preferredRedisValueFormat(loadedValue.data.content, readPreferredRedisValueFormat(), { allowJsonText }); + // Issue #10922: detected JSON opens pretty-printed, detected structured + // codecs open decoded — unless the user pinned an explicit format/codec. + // Bitmap payloads render through the string pipeline but are never JSON + // or a structured codec, so detection only runs for plain strings. + stringValueView.value = autoRedisValueFormat(detail, readStoredRedisValueFormat()); + if (allowJsonText) applyDetectedStringCodec(detail); + stringJsonRawBaseline.value = detail.json?.formattedText ?? ""; stringJsonDraftBaseline.value = jsonDraftBaseline(stringJsonRawBaseline.value, redisJsonDecoded.value); editValue.value = stringValueView.value === "json" && detail.json ? stringJsonDraftBaseline.value : detail.rawText; @@ -1936,7 +1984,8 @@ function selectMember(title: string, value: unknown, context: RedisMemberContext selectedMemberKey.value = getRedisMemberSelectionKey(title, value, identity); selectedMemberContext.value = context; isEditingMember.value = false; - memberValueView.value = preferredRedisValueFormat(value, readPreferredRedisValueFormat(), { allowJsonText: true }); + memberValueView.value = autoRedisValueFormat(detail, readStoredRedisValueFormat()); + applyDetectedMemberCodec(detail); memberJsonRawBaseline.value = detail.json?.formattedText ?? ""; memberJsonDraftBaseline.value = jsonDraftBaseline(memberJsonRawBaseline.value, redisJsonDecoded.value); memberEditValue.value = memberValueView.value === "json" && detail.json ? memberJsonDraftBaseline.value : detail.rawText; @@ -2889,6 +2938,7 @@ useUpdateBlocker(() => (hasUnsavedRedisDraft.value || editingTtl.value || saving {{ redisCodecLabel(codec) }} + {{ stringValueDetectionLabel }}