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 @@ 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)]