From 98d727e882db7300ae2d7003f8dc4aa0c1c8d1b6 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 29 Jul 2026 12:05:01 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(nacos-config):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E9=85=8D=E7=BD=AE=E5=8F=91=E5=B8=83=E7=9B=91=E5=90=AC?= =?UTF-8?q?=E8=AF=AF=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 发布前停止当前监听,避免自发布事件触发远端变更提示 - 发布失败时恢复监听,成功后由详情刷新重建监听 - 移除瞬态 dirty 标签并补充发布时序回归测试 --- .../components/NacosViewer.selection.test.tsx | 143 ++++++++++++++++++ frontend/src/components/NacosViewer.tsx | 49 ++++-- 2 files changed, 180 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/NacosViewer.selection.test.tsx b/frontend/src/components/NacosViewer.selection.test.tsx index 1e6613ad..447ccd59 100644 --- a/frontend/src/components/NacosViewer.selection.test.tsx +++ b/frontend/src/components/NacosViewer.selection.test.tsx @@ -48,6 +48,7 @@ const nacosBackend = vi.hoisted(() => ({ NacosImportConfigs: vi.fn(), NacosDeleteConfig: vi.fn(), NacosGetConfig: vi.fn(), + NacosPublishConfig: vi.fn(), NacosGetBetaConfig: vi.fn(), NacosStartConfigListen: vi.fn(), NacosStopConfigListen: vi.fn(), @@ -245,6 +246,7 @@ describe('NacosViewer config selection actions', () => { md5: 'test-md5', }, }); + nacosBackend.NacosPublishConfig.mockResolvedValue({ success: true }); nacosBackend.NacosGetBetaConfig.mockResolvedValue({ success: true, data: { exists: false }, @@ -775,6 +777,18 @@ describe('NacosViewer config selection actions', () => { expect(antdState.message.info).toHaveBeenCalledTimes(1); expect(renderedText(renderer!.toJSON())).not.toContain('Listening'); + const compactBanner = renderer!.root.find( + (node) => String(node.type) === 'alert', + ); + expect(compactBanner.props.className).toBe('gn-v2-nacos-banner'); + expect(renderedText(compactBanner)).toContain('Remote config update detected'); + expect(renderedText(compactBanner)).toContain('Reload remote'); + expect(renderedText(compactBanner)).toContain('Dismiss'); + expect(renderedText(compactBanner)).not.toContain('Local draft is clean'); + expect( + compactBanner.findByProps({ className: 'gn-v2-nacos-banner__copy' }), + ).toBeTruthy(); + await act(async () => { findButtonByExactText(renderer!, 'Reload remote')!.props.onClick(); }); @@ -858,6 +872,135 @@ describe('NacosViewer config selection actions', () => { renderer!.unmount(); }); + it('does not flash an internal dirty badge while publishing edited content', async () => { + const pendingPublish = deferred(); + nacosBackend.NacosPublishConfig.mockReturnValue(pendingPublish.promise); + + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create( + , + ); + }); + await flushEffects(); + + await act(async () => { + latestConfigTableProps().onRow(rows[0]).onClick(); + }); + await flushEffects(); + + await act(async () => { + renderer!.root.find( + (node) => (node.type as any) === 'nacos-editor', + ).props.onChange('server: changed'); + }); + + const publish = findButtonByExactText(renderer!, 'Publish'); + expect(publish?.props.disabled).toBe(false); + expect(renderedText(renderer!.toJSON())).not.toContain('dirty'); + + act(() => { + publish!.props.onClick(); + }); + await flushEffects(); + + expect(renderedText(renderer!.toJSON())).not.toContain('dirty'); + + pendingPublish.resolve({ success: true }); + await flushEffects(); + + expect(antdState.message.success).toHaveBeenCalledWith('Published successfully'); + expect(renderedText(renderer!.toJSON())).not.toContain('dirty'); + + await act(async () => { + renderer!.unmount(); + }); + }); + + it('does not report its own publish event as a remote config change', async () => { + const pendingListReload = deferred(); + let pageListCalls = 0; + nacosBackend.NacosSearchConfigs.mockImplementation( + async (_config: unknown, query: any) => { + if (query?.pageSize === 50) { + pageListCalls += 1; + if (pageListCalls === 2) return pendingListReload.promise; + } + return { + success: true, + data: { + totalCount: rows.length, + pageNumber: 1, + pagesAvailable: 1, + pageItems: rows, + }, + }; + }, + ); + nacosBackend.NacosStartConfigListen.mockResolvedValue({ + success: true, + data: { watchId: 'watch-publish' }, + }); + + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create( + , + ); + }); + await flushEffects(); + + await act(async () => { + latestConfigTableProps().onRow(rows[0]).onClick(); + }); + await flushEffects(); + + await act(async () => { + renderer!.root.find( + (node) => (node.type as any) === 'nacos-editor', + ).props.onChange('server: published locally'); + }); + act(() => { + findButtonByExactText(renderer!, 'Publish')!.props.onClick(); + }); + await flushEffects(); + + expect(antdState.message.success).toHaveBeenCalledWith('Published successfully'); + + await act(async () => { + runtimeState.configChangedHandler!({ + watchId: 'watch-publish', + connectionId: 'nacos-1', + namespaceId: 'dev', + group: rows[0].group, + dataId: rows[0].dataId, + }); + }); + await flushEffects(); + + const infoCallsDuringPublish = antdState.message.info.mock.calls.length; + const remoteBannerDuringPublish = renderedText(renderer!.toJSON()).includes( + 'Remote config update detected', + ); + + pendingListReload.resolve({ + success: true, + data: { + totalCount: rows.length, + pageNumber: 1, + pagesAvailable: 1, + pageItems: rows, + }, + }); + await flushEffects(); + await act(async () => { + renderer!.unmount(); + }); + + expect(infoCallsDuringPublish).toBe(0); + expect(remoteBannerDuringPublish).toBe(false); + }); + it('disables config import for restrictDataImport protection only', async () => { storeState.connections[0].config.protection.restrictDataImport = true; diff --git a/frontend/src/components/NacosViewer.tsx b/frontend/src/components/NacosViewer.tsx index d9879348..43e31505 100644 --- a/frontend/src/components/NacosViewer.tsx +++ b/frontend/src/components/NacosViewer.tsx @@ -762,8 +762,12 @@ const NacosViewer: React.FC = ({ message.warning(tr('nacos_viewer.message.beta_ips_required')); return; } + const publishTarget = detail; + const publishSelectionGeneration = selectionGenerationRef.current; + let publishSucceeded = false; setPublishing(true); try { + await stopListen(); const res = await (window as any).go.app.App.NacosPublishConfig(rpcConfig, { namespaceId: namespaceId || '', dataId: detail.dataId, @@ -775,9 +779,11 @@ const NacosViewer: React.FC = ({ betaIps: publishMode === 'beta' ? betaIps.trim() : '', }); if (!res?.success) { + void startListen(publishTarget, publishSelectionGeneration); message.error(res?.message || 'publish failed'); return; } + publishSucceeded = true; message.success( publishMode === 'beta' ? tr('nacos_viewer.message.beta_publish_success') @@ -787,12 +793,15 @@ const NacosViewer: React.FC = ({ setRemoteChanged(false); await loadList(pageNo); await loadDetail({ - dataId: detail.dataId, - group: detail.group, + dataId: publishTarget.dataId, + group: publishTarget.group, type: draftType, }); - await loadBetaMeta({ dataId: detail.dataId, group: detail.group }); + await loadBetaMeta({ dataId: publishTarget.dataId, group: publishTarget.group }); } catch (error: any) { + if (!publishSucceeded) { + void startListen(publishTarget, publishSelectionGeneration); + } message.error(error?.message || String(error)); } finally { setPublishing(false); @@ -1245,6 +1254,9 @@ const NacosViewer: React.FC = ({ ]; const namespaceLabel = namespaceName || (namespaceId ? namespaceId : 'public'); + const remoteChangedHint = draftDirty + ? tr('nacos_viewer.message.remote_changed_dirty_hint') + : tr('nacos_viewer.message.remote_changed_clean_hint'); return (
= ({ type="warning" showIcon className={isV2Ui ? 'gn-v2-nacos-banner' : undefined} - style={isV2Ui ? { margin: 0, borderRadius: 0 } : undefined} - message={tr('nacos_viewer.message.remote_changed_banner')} - description={ - draftDirty - ? tr('nacos_viewer.message.remote_changed_dirty_hint') - : tr('nacos_viewer.message.remote_changed_clean_hint') + message={ + isV2Ui ? ( + + + {tr('nacos_viewer.message.remote_changed_banner')} + + {draftDirty ? ( + + {remoteChangedHint} + + ) : null} + + ) : ( + tr('nacos_viewer.message.remote_changed_banner') + ) } + description={isV2Ui ? undefined : remoteChangedHint} action={ - + - @@ -1608,7 +1634,6 @@ const NacosViewer: React.FC = ({ {detail.group} {detail.dataId} {detail.md5 ? {detail.md5} : null} - {draftDirty ? dirty : null} {betaExists ? {tr('nacos_viewer.status.beta_active')} : null} ) : (