diff --git a/apps/desktop/src/components/nacos/NacosConfigBatchDialog.vue b/apps/desktop/src/components/nacos/NacosConfigBatchDialog.vue
index 0ae62ce109..24411beae0 100644
--- a/apps/desktop/src/components/nacos/NacosConfigBatchDialog.vue
+++ b/apps/desktop/src/components/nacos/NacosConfigBatchDialog.vue
@@ -1,11 +1,12 @@
@@ -302,7 +318,12 @@ watch(targetNamespaces, () => {
{{ item.group }} ยท {{ item.namespace || "public" }}
{{ item.message }}
- {{ batchStatusLabel(item.status) }}
+
+
+ {{ batchStatusLabel(item.status) }}
+
@@ -358,4 +379,6 @@ watch(targetNamespaces, () => {
+
+
diff --git a/apps/desktop/src/components/nacos/__tests__/NacosConfigBatchDialog.spec.ts b/apps/desktop/src/components/nacos/__tests__/NacosConfigBatchDialog.spec.ts
index ddad1f56ff..0d12c7008c 100644
--- a/apps/desktop/src/components/nacos/__tests__/NacosConfigBatchDialog.spec.ts
+++ b/apps/desktop/src/components/nacos/__tests__/NacosConfigBatchDialog.spec.ts
@@ -4,7 +4,20 @@ import { createApp, defineComponent, h, nextTick, type App } from "vue";
import { afterEach, describe, expect, it, vi } from "vitest";
import i18n from "@/i18n";
import NacosConfigBatchDialog from "@/components/nacos/NacosConfigBatchDialog.vue";
-import type { NacosBatchReport, NacosConfigKey } from "@/types/nacos";
+import type { NacosBatchPreview, NacosBatchReport, NacosConfigKey } from "@/types/nacos";
+
+vi.mock("@/components/nacos/NacosConfigDiffDialog.vue", () => ({
+ default: defineComponent({
+ props: {
+ open: Boolean,
+ before: String,
+ after: String,
+ format: String,
+ showConfirm: Boolean,
+ },
+ template: '
',
+ }),
+}));
const mountedApps: App[] = [];
@@ -16,6 +29,7 @@ async function mountDialog(
],
report: NacosBatchReport | null = null,
selectedKeys: NacosConfigKey[] = [{ namespace: "shared", group: "DEFAULT_GROUP", dataId: "app.yaml" }],
+ preview: NacosBatchPreview | null = null,
) {
const onTargetConnectionChange = vi.fn();
const onPreview = vi.fn();
@@ -39,7 +53,7 @@ async function mountDialog(
sourceConnectionId: "source",
currentNamespace: "shared",
namespaces,
- preview: null,
+ preview,
report,
onTargetConnectionChange,
onPreview,
@@ -232,4 +246,32 @@ describe("NacosConfigBatchDialog cross-connection sync", () => {
expect(document.body.textContent).toContain("Failed");
expect(document.body.textContent).toContain("target unavailable");
});
+
+ it("opens a read-only diff for a conflicting copy preview", async () => {
+ await mountDialog("remote", undefined, null, undefined, {
+ planHash: "preview-hash",
+ total: 1,
+ created: 0,
+ conflicts: 1,
+ invalid: 0,
+ items: [
+ {
+ namespace: "shared",
+ group: "DEFAULT_GROUP",
+ dataId: "app.yaml",
+ status: "conflict",
+ diff: { beforeContent: "port: 8080", afterContent: "port: 9090", format: "yaml" },
+ },
+ ],
+ });
+
+ (document.body.querySelector('[aria-label="Config Content Compare"]') as HTMLButtonElement).click();
+ await nextTick();
+
+ const diff = document.body.querySelector("[data-testid=nacos-preview-diff]") as HTMLElement;
+ expect(diff.dataset.before).toBe("port: 8080");
+ expect(diff.dataset.after).toBe("port: 9090");
+ expect(diff.dataset.format).toBe("yaml");
+ expect(diff.dataset.showConfirm).toBe("false");
+ });
});
diff --git a/apps/desktop/src/types/nacos.ts b/apps/desktop/src/types/nacos.ts
index 70a0d121ed..fe1c5b5a7d 100644
--- a/apps/desktop/src/types/nacos.ts
+++ b/apps/desktop/src/types/nacos.ts
@@ -358,12 +358,19 @@ export interface NacosConfigSelector {
export type NacosConflictPolicy = "ABORT" | "SKIP" | "OVERWRITE";
+export interface NacosBatchPreviewDiff {
+ beforeContent: string;
+ afterContent: string;
+ format?: string;
+}
+
export interface NacosBatchPreviewItem {
namespace: string;
group: string;
dataId: string;
status: string;
message?: string;
+ diff?: NacosBatchPreviewDiff;
}
export interface NacosBatchPreview {
diff --git a/crates/dbx-core/src/nacos/batch.rs b/crates/dbx-core/src/nacos/batch.rs
index 8910a1dbbb..95d1415d26 100644
--- a/crates/dbx-core/src/nacos/batch.rs
+++ b/crates/dbx-core/src/nacos/batch.rs
@@ -11,8 +11,9 @@ use crate::nacos::port::NacosAdmin;
use crate::nacos::search::{begin_operation, finish_operation};
use crate::nacos::service::{ensure_connection_writable, get_admin};
use crate::nacos::types::{
- NacosBatchItemResult, NacosBatchPreview, NacosBatchPreviewItem, NacosBatchReport, NacosConfigItem, NacosConfigKey,
- NacosConfigSelectionScope, NacosConfigSelector, NacosConfigTransferRequest, NacosConfigUpsert, NacosConflictPolicy,
+ NacosBatchItemResult, NacosBatchPreview, NacosBatchPreviewDiff, NacosBatchPreviewItem, NacosBatchReport,
+ NacosConfigItem, NacosConfigKey, NacosConfigSelectionScope, NacosConfigSelector, NacosConfigTransferRequest,
+ NacosConfigUpsert, NacosConflictPolicy,
};
const PAGE_SIZE: u32 = 500;
@@ -64,7 +65,7 @@ pub async fn preview_import(
archive_path: &Path,
) -> Result {
let configs = read_archive(archive_path, target_namespace).await?;
- preview_configs(admin, &configs).await
+ preview_configs(admin, &configs, false).await
}
pub async fn apply_import(
@@ -85,7 +86,7 @@ pub async fn preview_transfer(
request: &NacosConfigTransferRequest,
) -> Result {
let configs = transfer_configs(source_admin, request).await?;
- preview_configs(target_admin, &configs).await
+ preview_configs(target_admin, &configs, true).await
}
pub async fn apply_transfer(
@@ -330,6 +331,7 @@ async fn resolve_selector(
async fn preview_configs(
admin: Arc,
configs: &[NacosConfigUpsert],
+ include_diff: bool,
) -> Result {
let mut items = Vec::with_capacity(configs.len());
let mut target_snapshots = Vec::with_capacity(configs.len());
@@ -344,6 +346,17 @@ async fn preview_configs(
created += 1;
"create"
};
+ let diff = if include_diff {
+ target.as_ref().and_then(|target| {
+ target.content.as_ref().map(|before_content| NacosBatchPreviewDiff {
+ before_content: before_content.clone(),
+ after_content: config.content.clone(),
+ format: config.config_type.clone(),
+ })
+ })
+ } else {
+ None
+ };
target_snapshots.push(target);
items.push(NacosBatchPreviewItem {
namespace: config.namespace.clone().unwrap_or_default(),
@@ -351,6 +364,7 @@ async fn preview_configs(
data_id: config.data_id.clone(),
status: status.to_string(),
message: None,
+ diff,
});
}
Ok(NacosBatchPreview {
@@ -370,7 +384,7 @@ async fn apply_configs(
expected_plan_hash: &str,
policy: &NacosConflictPolicy,
) -> Result {
- let preview = preview_configs(admin.clone(), &configs).await?;
+ let preview = preview_configs(admin.clone(), &configs, false).await?;
if preview.plan_hash != expected_plan_hash {
return Err(
"NACOS_ERROR[stalePreview]: Nacos import/copy preview is stale; preview again before applying".to_string()
@@ -750,7 +764,7 @@ mod tests {
}
async fn preview_hash(admin: Arc, configs: &[NacosConfigUpsert]) -> String {
- preview_configs(admin, configs).await.unwrap().plan_hash
+ preview_configs(admin, configs, false).await.unwrap().plan_hash
}
#[tokio::test]
@@ -1107,4 +1121,30 @@ mod tests {
.unwrap();
assert_eq!(published.content.as_deref(), Some("value"));
}
+
+ #[tokio::test]
+ async fn transfer_preview_includes_conflict_content_diff() {
+ let source_admin = source_admin_with_config();
+ let target_admin = Arc::new(MockAdmin::default());
+ target_admin.insert(NacosConfigItem {
+ data_id: "app".to_string(),
+ group: "SOURCE_GROUP".to_string(),
+ namespace: "target-ns".to_string(),
+ app_name: None,
+ desc: None,
+ tags: None,
+ config_type: Some("text".to_string()),
+ md5: Some("target-md5".to_string()),
+ encrypted_data_key: None,
+ content: Some("old value".to_string()),
+ });
+
+ let preview = preview_transfer(source_admin, target_admin, &transfer_request(None)).await.unwrap();
+ let diff = preview.items[0].diff.as_ref().expect("conflicting transfer should include a diff");
+
+ assert_eq!(preview.items[0].status, "conflict");
+ assert_eq!(diff.before_content, "old value");
+ assert_eq!(diff.after_content, "value");
+ assert_eq!(diff.format.as_deref(), Some("text"));
+ }
}
diff --git a/crates/dbx-core/src/nacos/types.rs b/crates/dbx-core/src/nacos/types.rs
index f13003e9c7..dab556dfa3 100644
--- a/crates/dbx-core/src/nacos/types.rs
+++ b/crates/dbx-core/src/nacos/types.rs
@@ -766,6 +766,15 @@ pub enum NacosConflictPolicy {
Overwrite,
}
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct NacosBatchPreviewDiff {
+ pub before_content: String,
+ pub after_content: String,
+ #[serde(default)]
+ pub format: Option,
+}
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NacosBatchPreviewItem {
@@ -775,6 +784,8 @@ pub struct NacosBatchPreviewItem {
pub status: String,
#[serde(default)]
pub message: Option,
+ #[serde(default)]
+ pub diff: Option,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]