From fe269a28db5865bd54fbbc3fa26bce946e704899 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sat, 22 Aug 2026 07:17:40 +0000 Subject: [PATCH] feat(zookeeper): add UTF-8 value preview Closes #6897 --- .../src/components/kv/KvKeyBrowser.vue | 57 ++++++++++++++++-- .../KvKeyBrowserValuePreview.spec.ts | 26 +++++++++ .../zookeeper/ZooKeeperKeyBrowser.vue | 4 +- .../__tests__/ZooKeeperKeyBrowser.spec.ts | 58 +++++++++++++++++++ apps/desktop/src/i18n/locales/en.ts | 2 + apps/desktop/src/i18n/locales/es.ts | 2 + apps/desktop/src/i18n/locales/it.ts | 2 + apps/desktop/src/i18n/locales/ja.ts | 2 + apps/desktop/src/i18n/locales/ko.ts | 2 + apps/desktop/src/i18n/locales/pt-BR.ts | 2 + apps/desktop/src/i18n/locales/zh-CN.ts | 2 + apps/desktop/src/i18n/locales/zh-TW.ts | 2 + .../lib/__tests__/kv/kvValueDisplay.spec.ts | 25 +++++++- apps/desktop/src/lib/kv/kvValueDisplay.ts | 29 ++++++++++ 14 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 apps/desktop/src/components/kv/__tests__/KvKeyBrowserValuePreview.spec.ts create mode 100644 apps/desktop/src/components/zookeeper/__tests__/ZooKeeperKeyBrowser.spec.ts diff --git a/apps/desktop/src/components/kv/KvKeyBrowser.vue b/apps/desktop/src/components/kv/KvKeyBrowser.vue index 028ffff0d8..fb20fd8a44 100644 --- a/apps/desktop/src/components/kv/KvKeyBrowser.vue +++ b/apps/desktop/src/components/kv/KvKeyBrowser.vue @@ -24,7 +24,7 @@ import { decideKvMetadataRefresh, hasPositiveKvLease, knownKvLeaseSummaries, mer import { classifyKvMutationError, type KvMutationErrorKind } from "@/lib/kv/kvMutationError"; import { refreshedKvSelectionSummary } from "@/lib/kv/kvRefreshSelection"; import { parseKvLeaseId, parseOptionalTtl } from "@/lib/kv/kvTtl"; -import { formatZooKeeperMetadataRows, formatZooKeeperSummaryBadges, prettyPrintJsonText } from "@/lib/kv/kvValueDisplay"; +import { decodeBase64Utf8Preview, formatZooKeeperMetadataRows, formatZooKeeperSummaryBadges, prettyPrintJsonText } from "@/lib/kv/kvValueDisplay"; import { formatTtl } from "@/lib/common/ttlFormat"; import { createLazyKvKeyTreeState, @@ -118,6 +118,8 @@ interface KvKeyBrowserLabels { copy?: string; copied?: string; copyFailed?: string; + utf8PreviewLossy?: string; + utf8PreviewUnavailable?: string; valueTooLarge?: string; deletePrefix?: string; selectAll?: string; @@ -195,6 +197,7 @@ const props = withDefaults( safeWrite?: boolean; maxValueBytes?: number; allowBinaryEdit?: boolean; + enableBase64Utf8Preview?: boolean; readOnly?: boolean; exportFormat?: string; exportFileExtension?: string; @@ -221,6 +224,7 @@ const props = withDefaults( safeWrite: false, maxValueBytes: 0, allowBinaryEdit: false, + enableBase64Utf8Preview: false, readOnly: false, exportFormat: "dbx-kv-bundle", exportFileExtension: ".dbx-kv.json", @@ -290,6 +294,7 @@ const showHistoryDiff = ref(false); const restoring = ref(false); const selectedCreateMode = ref("persistent"); const selectedPrettyValue = ref(null); +const selectedBase64ViewMode = ref<"utf8" | "base64">("utf8"); const selectedValueCopied = ref(false); const lazyTreeState = reactive(createLazyKvKeyTreeState(lazyKvRootPath(props.lazyPathStyle), props.lazyPathStyle)); const multiSelectedKeys = ref>(new Map()); @@ -385,8 +390,28 @@ const selectedTextValue = computed(() => { if (!value) return ""; return value.encoding === "utf8" ? value.data : value.data; }); -const displayedSelectedTextValue = computed(() => selectedPrettyValue.value ?? selectedTextValue.value); const selectedValueIsBase64 = computed(() => selectedValue.value?.value?.encoding === "base64"); +const selectedBase64Utf8Preview = computed(() => { + if (!props.enableBase64Utf8Preview || !selectedValueIsBase64.value) return null; + return decodeBase64Utf8Preview(selectedTextValue.value); +}); +const showSelectedBase64Utf8Controls = computed(() => props.enableBase64Utf8Preview && selectedValueIsBase64.value); +const selectedBase64DisplayMode = computed<"utf8" | "base64">(() => (selectedBase64ViewMode.value === "utf8" && selectedBase64Utf8Preview.value?.ok ? "utf8" : "base64")); +const selectedValueUsesUtf8Preview = computed(() => showSelectedBase64Utf8Controls.value && selectedBase64DisplayMode.value === "utf8"); +const selectedBase64Utf8PreviewIsLossy = computed(() => { + const preview = selectedBase64Utf8Preview.value; + return preview?.ok === true && preview.lossy; +}); +const selectedBase64Utf8PreviewUnavailable = computed(() => selectedBase64Utf8Preview.value?.ok === false); +const displayedSelectedTextValue = computed(() => { + if (selectedPrettyValue.value != null) return selectedPrettyValue.value; + const preview = selectedBase64Utf8Preview.value; + return selectedValueUsesUtf8Preview.value && preview?.ok ? preview.value : selectedTextValue.value; +}); +const selectedValueClipboardText = computed(() => { + const preview = selectedBase64Utf8Preview.value; + return selectedValueUsesUtf8Preview.value && preview?.ok ? preview.value : selectedTextValue.value; +}); const selectedKeyBytes = computed(() => selectedValue.value?.keyBytes ?? selectedRouteKeyBytes.value ?? null); const selectedKeyLocked = computed(() => Boolean(String(selectedMetadata.value?.session ?? "").trim())); const isWatchingSelectedKey = computed(() => Boolean(selectedKey.value && props.watchActiveKey === selectedKey.value)); @@ -856,6 +881,7 @@ async function loadSelectedKey(input: string | KvKeyRoute) { selectedRouteKeyBytes.value = route.keyBytes ?? null; selectedValue.value = null; selectedPrettyValue.value = null; + selectedBase64ViewMode.value = "utf8"; detailLoading.value = true; detailError.value = ""; try { @@ -891,6 +917,7 @@ function clearSelectedKey() { selectedRouteKeyBytes.value = null; selectedValue.value = null; selectedPrettyValue.value = null; + selectedBase64ViewMode.value = "utf8"; detailLoading.value = false; } @@ -1364,7 +1391,7 @@ async function copyText(value: string | number | null | undefined) { async function copySelectedValue() { if (!selectedValue.value?.found) return; try { - await copyToClipboard(selectedTextValue.value); + await copyToClipboard(selectedValueClipboardText.value); selectedValueCopied.value = true; toast(props.labels.copied || "Copied", 1500); if (selectedValueCopyTimer) clearTimeout(selectedValueCopyTimer); @@ -2025,7 +2052,23 @@ defineExpose({
-
{{ labels.value || "Value" }}
+
+
{{ labels.value || "Value" }}
+
+ + +
+
+
+ {{ labels.utf8PreviewLossy || "Invalid UTF-8 bytes are shown as replacement characters (�). The original Base64 value is unchanged." }} +
+
+ {{ labels.utf8PreviewUnavailable || "The Base64 value is invalid, so the original value is shown." }} +