🐛 fix(nacos-config): 修复配置发布监听误报

- 发布前停止当前监听,避免自发布事件触发远端变更提示
- 发布失败时恢复监听,成功后由详情刷新重建监听
- 移除瞬态 dirty 标签并补充发布时序回归测试
This commit is contained in:
Syngnat
2026-07-29 12:05:01 +08:00
parent 21951ca08a
commit 98d727e882
2 changed files with 180 additions and 12 deletions

View File

@@ -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<any>();
nacosBackend.NacosPublishConfig.mockReturnValue(pendingPublish.promise);
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(
<NacosViewer connectionId="nacos-1" namespaceId="dev" namespaceName="dev" />,
);
});
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<any>();
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(
<NacosViewer connectionId="nacos-1" namespaceId="dev" namespaceName="dev" />,
);
});
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;

View File

@@ -762,8 +762,12 @@ const NacosViewer: React.FC<NacosViewerProps> = ({
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<NacosViewerProps> = ({
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<NacosViewerProps> = ({
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<NacosViewerProps> = ({
];
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 (
<div
@@ -1266,19 +1278,33 @@ const NacosViewer: React.FC<NacosViewerProps> = ({
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 ? (
<span className="gn-v2-nacos-banner__copy">
<span className="gn-v2-nacos-banner__title">
{tr('nacos_viewer.message.remote_changed_banner')}
</span>
{draftDirty ? (
<span className="gn-v2-nacos-banner__hint" title={remoteChangedHint}>
{remoteChangedHint}
</span>
) : null}
</span>
) : (
tr('nacos_viewer.message.remote_changed_banner')
)
}
description={isV2Ui ? undefined : remoteChangedHint}
action={
<Space>
<Space size={4}>
<Button size="small" type="primary" onClick={() => void handleReloadRemote()}>
{tr('nacos_viewer.action.reload_remote')}
</Button>
<Button size="small" onClick={() => setRemoteChanged(false)}>
<Button
size="small"
type={isV2Ui ? 'text' : 'default'}
onClick={() => setRemoteChanged(false)}
>
{tr('nacos_viewer.action.dismiss_remote')}
</Button>
</Space>
@@ -1608,7 +1634,6 @@ const NacosViewer: React.FC<NacosViewerProps> = ({
<Tag>{detail.group}</Tag>
<strong style={{ color: workbenchTheme.textPrimary }}>{detail.dataId}</strong>
{detail.md5 ? <Tag color="default">{detail.md5}</Tag> : null}
{draftDirty ? <Tag color="orange">dirty</Tag> : null}
{betaExists ? <Tag color="purple">{tr('nacos_viewer.status.beta_active')}</Tag> : null}
</>
) : (