fix(admin): dedupe device-link conflict candidates by ciId
The reconciler can emit the same configuration_item id multiple times when more than one match rule fires (e.g. exact_serial AND hostname_in_company). The page rendered each occurrence as a separate row keyed on ciId, producing React duplicate-key errors. Dedupe client-side keeping the strongest confidence (exact_serial > mac > hostname_in_company > other) and use the deduped count in the badge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c97e5fc45c
commit
43fbe7b0e1
1 changed files with 28 additions and 4 deletions
|
|
@ -55,6 +55,27 @@ function confidenceColor(c: string | null): 'default' | 'secondary' | 'outline'
|
|||
return 'outline';
|
||||
}
|
||||
|
||||
const CONFIDENCE_RANK: Record<string, number> = {
|
||||
exact_serial: 0,
|
||||
mac: 1,
|
||||
hostname_in_company: 2,
|
||||
};
|
||||
|
||||
function dedupeCandidates(candidates: Candidate[]): Candidate[] {
|
||||
const byId = new Map<string, Candidate>();
|
||||
for (const c of candidates) {
|
||||
const existing = byId.get(c.ciId);
|
||||
if (!existing) {
|
||||
byId.set(c.ciId, c);
|
||||
continue;
|
||||
}
|
||||
const a = CONFIDENCE_RANK[existing.confidence ?? ''] ?? 99;
|
||||
const b = CONFIDENCE_RANK[c.confidence ?? ''] ?? 99;
|
||||
if (b < a) byId.set(c.ciId, c);
|
||||
}
|
||||
return Array.from(byId.values());
|
||||
}
|
||||
|
||||
export default function DeviceLinkConflictsPage() {
|
||||
const [items, setItems] = useState<Review[] | null>(null);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
|
@ -172,7 +193,9 @@ export default function DeviceLinkConflictsPage() {
|
|||
</Alert>
|
||||
)}
|
||||
|
||||
{items?.map((r) => (
|
||||
{items?.map((r) => {
|
||||
const candidates = dedupeCandidates(r.candidates);
|
||||
return (
|
||||
<Card key={r.id} className="border-amber-200">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
|
|
@ -190,7 +213,7 @@ export default function DeviceLinkConflictsPage() {
|
|||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{r.candidates.length} candidates
|
||||
{candidates.length} candidates
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground space-x-3">
|
||||
|
|
@ -202,7 +225,7 @@ export default function DeviceLinkConflictsPage() {
|
|||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{r.candidates.map((c) => {
|
||||
{candidates.map((c) => {
|
||||
const isResolving = resolving === `${r.id}:${c.ciId}`;
|
||||
return (
|
||||
<div
|
||||
|
|
@ -253,7 +276,8 @@ export default function DeviceLinkConflictsPage() {
|
|||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue