fix(frontend): key the node table by the computed row key, not id

The desktop node table used rowKey="id", but transitive sub-nodes (the
read-only rows surfaced from downstream nodes) all carry id 0, so a topology
with two or more transitive rows gave React duplicate keys. antd's rowKey
prop overrides the row object's own computed `key` (`t-${guid}` for
transitive rows, the numeric id otherwise), so the unique key the code
already builds was ignored — causing row-state/DOM mis-association on any
re-render (heartbeat refetch, address-eye toggle). The mobile card path
already keyed by record.key.

Key the table by "key" so transitive rows get their distinct t-${guid}
identity; direct nodes keep key === id, so row selection (filtered to numeric
keys) is unchanged.
This commit is contained in:
MHSanaei
2026-07-15 05:31:42 +02:00
parent d2ba6c9fc2
commit 8662c31296
2 changed files with 47 additions and 1 deletions
+1 -1
View File
@@ -650,7 +650,7 @@ export default function NodeList({
loading={loading}
scroll={{ x: 'max-content' }}
size="middle"
rowKey="id"
rowKey="key"
rowSelection={dataSource.length > 1 ? {
selectedRowKeys: selectedIds,
onChange: (keys) => onSelectionChange(keys.filter((k) => typeof k === 'number') as number[]),
@@ -0,0 +1,46 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import NodeList from '@/pages/nodes/NodeList';
import type { NodeRecord } from '@/schemas/node';
import { renderWithProviders } from './test-utils';
const noop = () => {};
function sampleNodes(): NodeRecord[] {
return [
{ id: 1, name: 'parent', guid: 'p1', transitive: false, enable: true, status: 'online' },
{ id: 0, name: 'child-a', guid: 'ca', parentGuid: 'p1', transitive: true },
{ id: 0, name: 'child-b', guid: 'cb', parentGuid: 'p1', transitive: true },
];
}
describe('NodeList desktop table row keys', () => {
afterEach(() => vi.restoreAllMocks());
it('gives transitive sub-node rows distinct keys instead of colliding on id 0', () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
renderWithProviders(
<NodeList
nodes={sampleNodes()}
isMobile={false}
selectedIds={[]}
onSelectionChange={noop}
onAdd={noop}
onMtls={noop}
onEdit={noop}
onDelete={noop}
onProbe={noop}
onToggleEnable={noop}
onUpdateNode={noop}
onUpdateSelected={noop}
/>,
);
const duplicateKeyWarning = errorSpy.mock.calls.some((call) =>
call.some((arg) => typeof arg === 'string' && arg.includes('same key')),
);
expect(duplicateKeyWarning).toBe(false);
});
});