fix(kv): hide internal agent error data in browser messages
This commit is contained in:
1 parent
5fa0c5a56d
commit
19be861c49
4 files changed
+46
-21
No files matched your search
@@ -14,6 +14,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useTabUiState } from "@/lib/tabs/tabUiState";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { formatError } from "@/lib/backend/errorUtils";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { isKeyInKvExportScope, kvExportFilenameStem, kvValueByteIdentity, type KvExportScopeKind, type KvExportScopeRequest } from "@/lib/kv/kvExportScope";
|
||||
import { detectKvValueFormat } from "@/lib/kv/kvValueFormat";
|
||||
@@ -596,7 +597,7 @@ async function exportTreeSelection(format: EtcdExportFormat) {
|
||||
const exported = await downloadExport(file);
|
||||
if (exported) toast(t("etcd.exported", { count: entries.length }), 2500);
|
||||
} catch (error) {
|
||||
toast(error instanceof Error ? error.message : String(error), 4000);
|
||||
toast(formatError(error), 4000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,7 +624,7 @@ async function deleteSelectedTreeKeys() {
|
||||
toast(t("etcd.batchDeleteSuccess", { count: deleted }), 3000);
|
||||
} catch (error) {
|
||||
failed = true;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const message = formatError(error);
|
||||
toast(t("etcd.batchDeletePartial", { count: deleted, error: message }), 5000);
|
||||
} finally {
|
||||
if (failed) browserRef.value?.clearMultiSelection();
|
||||
@@ -711,7 +712,7 @@ async function exportAll(format: EtcdExportFormat) {
|
||||
const exported = await downloadExport(file);
|
||||
if (exported) toast(t("etcd.exported", { count: scan.entries.length }), 2500);
|
||||
} catch (error) {
|
||||
toast(error instanceof Error ? error.message : String(error), 4000);
|
||||
toast(formatError(error), 4000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -744,7 +745,7 @@ async function exportEtcdNodeScope(connectionId: string, request: KvExportScopeR
|
||||
const exported = await downloadExport(file);
|
||||
if (exported) toast(t("etcd.exported", { count: entries.length }), 2500);
|
||||
} catch (error) {
|
||||
toast(error instanceof Error ? error.message : String(error), 4000);
|
||||
toast(formatError(error), 4000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -776,7 +777,7 @@ async function onImportFile(event: Event) {
|
||||
transferOpen.value = true;
|
||||
await previewTransfer();
|
||||
} catch (error) {
|
||||
toast(error instanceof Error ? error.message : String(error), 4000);
|
||||
toast(formatError(error), 4000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -825,7 +826,7 @@ async function loadSyncPreview() {
|
||||
transferBundle.value = bundleFromSummaries(scan.entries, prefix, scan.revision);
|
||||
await previewTransfer();
|
||||
} catch (error) {
|
||||
transferError.value = error instanceof Error ? error.message : String(error);
|
||||
transferError.value = formatError(error);
|
||||
} finally {
|
||||
transferLoading.value = false;
|
||||
transferLoadingDetail.value = "";
|
||||
@@ -870,7 +871,7 @@ async function previewTransfer() {
|
||||
if (transferMode.value === "sync") syncConfigurationExpanded.value = false;
|
||||
} catch (error) {
|
||||
if (generation !== transferPreviewGeneration) return;
|
||||
transferError.value = error instanceof Error ? error.message : String(error);
|
||||
transferError.value = formatError(error);
|
||||
transferPreviewLoaded.value = false;
|
||||
} finally {
|
||||
if (generation === transferPreviewGeneration) {
|
||||
@@ -915,7 +916,7 @@ async function applyTransfer() {
|
||||
transferOpen.value = false;
|
||||
if (targetId === props.connectionId) browserRef.value?.refresh();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const message = formatError(error);
|
||||
// Rebuild the preview so an ambiguous network failure or a successful
|
||||
// prefix of the batch is reflected before the user retries.
|
||||
let previewError = "";
|
||||
@@ -1032,7 +1033,7 @@ async function runSearch() {
|
||||
return { id: `${identity}:${summary.modRevision || ""}`, displayKey: shown, keyIdentity: identity, summary: { ...summary, key: shown, keyBytes: bytes, keyIdentity: identity }, matchesKey, matchesValue, selected: true };
|
||||
});
|
||||
} catch (error) {
|
||||
searchError.value = error instanceof Error ? error.message : String(error);
|
||||
searchError.value = formatError(error);
|
||||
} finally {
|
||||
searchRunning.value = false;
|
||||
}
|
||||
@@ -1063,7 +1064,7 @@ async function openOperations(nextMode: Extract<WorkbenchMode, "maintenance" | "
|
||||
try {
|
||||
operationsStatus.value = await api.etcdStatus(props.connectionId);
|
||||
} catch (error) {
|
||||
toast(error instanceof Error ? error.message : String(error), 5000);
|
||||
toast(formatError(error), 5000);
|
||||
} finally {
|
||||
operationsLoading.value = false;
|
||||
}
|
||||
@@ -1083,7 +1084,7 @@ async function refreshLeaseOptions() {
|
||||
try {
|
||||
leaseOptions.value = (await api.etcdLeaseList(props.connectionId)).leases;
|
||||
} catch (error) {
|
||||
toast(error instanceof Error ? error.message : String(error), 4000);
|
||||
toast(formatError(error), 4000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1099,7 +1100,7 @@ async function exportSearchResults(format: EtcdExportFormat) {
|
||||
const exported = await downloadExport(file);
|
||||
if (exported) toast(t("etcd.exported", { count: selected.length }), 2500);
|
||||
} catch (error) {
|
||||
toast(error instanceof Error ? error.message : String(error), 4000);
|
||||
toast(formatError(error), 4000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomC
|
||||
import NacosConfigDiffDialog from "@/components/nacos/NacosConfigDiffDialog.vue";
|
||||
import KvValueEditor from "@/components/kv/KvValueEditor.vue";
|
||||
import type { KvCreateMode, KvDeleteOptions, KvGetOptions, KvGetResponse, KvHistoryEvent, KvHistoryResponse, KvInt64, KvKeySummary, KvListPrefixOptions, KvPutOptions, KvPutResponse, KvValue } from "@/lib/backend/api";
|
||||
import { formatError } from "@/lib/backend/errorUtils";
|
||||
import type { KvExportScopeRequest } from "@/lib/kv/kvExportScope";
|
||||
import { buildKvKeyTree, flattenVisibleKvKeyTree, kvKeyTreeNodePath, preserveKvExpandedGroupIds, type KvKeyTreeNode } from "@/lib/kv/kvKeyTree";
|
||||
import { decideKvMetadataRefresh, hasPositiveKvLease, knownKvLeaseSummaries, mergeKvKeyMetadata, mergeKvValueRefresh, nextKvLeaseRefreshDelay, removeMissingKvKey, updateKvResponseTtl } from "@/lib/kv/kvMetadataRefresh";
|
||||
@@ -712,7 +713,7 @@ async function loadKeys(reset = true, options: LoadKeysOptions = {}) {
|
||||
}
|
||||
} catch (error) {
|
||||
if (reset && generation === keyLoadGeneration && props.connectionId === connectionId) {
|
||||
listError.value = error instanceof Error ? error.message : String(error);
|
||||
listError.value = formatError(error);
|
||||
}
|
||||
} finally {
|
||||
if (generation === keyLoadGeneration && props.connectionId === connectionId) {
|
||||
@@ -792,7 +793,7 @@ async function loadLazyRoot(reset = true, options: LoadKeysOptions = {}) {
|
||||
}
|
||||
} catch (error) {
|
||||
if (lazyLoadContextValid(context)) {
|
||||
listError.value = error instanceof Error ? error.message : String(error);
|
||||
listError.value = formatError(error);
|
||||
}
|
||||
} finally {
|
||||
if (lazyLoadContextValid(context)) loading.value = false;
|
||||
@@ -966,7 +967,7 @@ async function loadSelectedKey(input: string | KvKeyRoute) {
|
||||
startKeyListRefresh();
|
||||
} catch (error) {
|
||||
if (requestId !== detailRequestId || selectedKey.value !== key || connectionId !== props.connectionId) return;
|
||||
detailError.value = error instanceof Error ? error.message : String(error);
|
||||
detailError.value = formatError(error);
|
||||
} finally {
|
||||
if (requestId === detailRequestId && connectionId === props.connectionId) detailLoading.value = false;
|
||||
}
|
||||
@@ -1414,7 +1415,7 @@ async function deleteSelectedKey() {
|
||||
}
|
||||
toast(props.labels.deleted, 2500);
|
||||
} catch (error) {
|
||||
detailError.value = error instanceof Error ? error.message : String(error);
|
||||
detailError.value = formatError(error);
|
||||
showDeleteConfirm.value = false;
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
@@ -1471,7 +1472,7 @@ async function copySelectedValue() {
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
selectedValueCopied.value = false;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const message = formatError(error);
|
||||
const failureTemplate = props.labels.copyFailed || "Copy failed: {message}";
|
||||
toast(failureTemplate.includes("{message}") ? failureTemplate.replace("{message}", message) : `${failureTemplate}: ${message}`, 3500);
|
||||
}
|
||||
@@ -1603,7 +1604,7 @@ async function moveOrCopySelectedKey() {
|
||||
await loadSelectedKey({ key: next, keyIdentity: next, keyBytes: { encoding: "utf8", data: next } });
|
||||
toast(props.labels.saved, 2500);
|
||||
} catch (error) {
|
||||
renameError.value = error instanceof Error ? error.message : String(error);
|
||||
renameError.value = formatError(error);
|
||||
} finally {
|
||||
renaming.value = false;
|
||||
}
|
||||
@@ -1626,7 +1627,7 @@ async function openHistory() {
|
||||
historyEvents.value = response.events;
|
||||
} catch (error) {
|
||||
historyEvents.value = [];
|
||||
historyError.value = error instanceof Error ? error.message : String(error);
|
||||
historyError.value = formatError(error);
|
||||
} finally {
|
||||
historyLoading.value = false;
|
||||
}
|
||||
@@ -1650,7 +1651,7 @@ async function restoreHistory() {
|
||||
await loadKeys(true, { preserveSelection: true });
|
||||
toast(props.labels.saved, 2500);
|
||||
} catch (error) {
|
||||
historyError.value = error instanceof Error ? error.message : String(error);
|
||||
historyError.value = formatError(error);
|
||||
} finally {
|
||||
restoring.value = false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatError, sanitizeBackendErrorMessage } from "@/lib/backend/errorUtils";
|
||||
|
||||
// Reported in #8860: an etcd revision that has already been compacted surfaces
|
||||
// its internal Agent contract payload in the history dialog.
|
||||
const COMPACTED_HISTORY_ERROR =
|
||||
'ETCD_COMPACTED: requested history was compacted at revision 6538169\nDBX_AGENT_ERROR_DATA:{"contractVersion":1,"category":"protocol","retryable":false,"sessionDisposition":"keep","stage":"execute","operationOutcome":"unknown","agentSessionId":null,"sqlState":null,"vendorCode":null,"exceptionClass":"ETCD_COMPACTED:requestedhistorywascompactedatrevision6538169"}';
|
||||
|
||||
describe("KV browser error presentation", () => {
|
||||
it("keeps the internal Agent payload out of a compacted-revision history error", () => {
|
||||
const expected = "ETCD_COMPACTED: requested history was compacted at revision 6538169";
|
||||
|
||||
expect(sanitizeBackendErrorMessage(COMPACTED_HISTORY_ERROR)).toBe(expected);
|
||||
expect(formatError(new Error(COMPACTED_HISTORY_ERROR))).toBe(expected);
|
||||
});
|
||||
|
||||
it("leaves an error without the internal marker untouched", () => {
|
||||
const message = "etcdserver: permission denied";
|
||||
|
||||
expect(formatError(new Error(message))).toBe(message);
|
||||
expect(formatError(message)).toBe(message);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -42,7 +42,7 @@ describe("KvKeyBrowser search and split layout", () => {
|
||||
|
||||
it("shows list request errors instead of the empty-state message", () => {
|
||||
expect(browserSource).toContain('const listError = ref("");');
|
||||
expect(browserSource.match(/listError\.value = error instanceof Error \? error\.message : String\(error\);/g)).toHaveLength(2);
|
||||
expect(browserSource.match(/listError\.value = formatError\(error\);/g)).toHaveLength(2);
|
||||
expect(browserSource.indexOf('v-else-if="listError"')).toBeLessThan(browserSource.indexOf('v-else-if="visibleRows.length === 0"'));
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user