Asynchroner Tooltip-Inhalt
Wenn die Daten eines Knotens eine Referenz auf einen Datensatz und nicht auf den Datensatz selbst sind, liegt der interessante Inhalt hinter einem HTTP-Aufruf. Geben Sie ein Versprechen von einem Inhalts-Hook zurück und die Bibliothek kümmert sich um den Rest: Sie zeigt einen Platzhalter an, tauscht den Inhalt ein, wenn er aufgelöst wird, und löscht das Ergebnis, wenn der Tooltip inzwischen auf einen anderen Knoten verschoben wurde.
Bewegen Sie den Mauszeiger über einen Knoten und warten Sie – die Anreicherungschips treffen nach etwa 900 ms ein. Bewegen Sie dann den Mauszeiger über einen Knoten und wechseln Sie sofort zum nächsten: Die abgebrochene Anfrage wird über ctx.signal abgebrochen und das verspätete Ergebnis landet nie in der Tooltip, die Sie betrachten.
const options = {
UI: {
tooltip: {
nodeHeaderMap: {
title: (node) => node.getData().label,
subtitle: (node) => node.getData().type
},
// Return a promise and the tooltip shows a placeholder until it resolves.
// Hover from node to node: the abandoned fetch is aborted, and its result
// is dropped rather than painted into the tooltip you are now looking at.
renderNodeExtra: async (node, { signal }) => {
const { tags, sightings } = await fetchEnrichment(node.getData().uuid, signal)
const box = document.createElement('div')
box.style.cssText = 'margin-top:6px;display:flex;gap:4px;flex-wrap:wrap;align-items:center'
for (const tag of tags) {
const chip = document.createElement('span')
chip.textContent = tag
chip.style.cssText = 'font-size:11px;padding:1px 6px;border-radius:9px;background:#f0ad4e33;border:1px solid #f0ad4e'
box.appendChild(chip)
}
const count = document.createElement('span')
count.textContent = `· ${sightings} sightings`
count.style.cssText = 'font-size:11px;opacity:.75'
box.appendChild(count)
return box
}
},
// Optional: replace the built-in skeleton and error line.
asyncContent: {
placeholder: () => {
const line = document.createElement('div')
line.textContent = 'Loading enrichment…'
line.style.cssText = 'margin-top:6px;font-size:11px;opacity:.6'
return line
},
error: 'Enrichment unavailable'
}
}
}// Stands in for a real endpoint. A genuine integration would `fetch(url, { signal })`
// — the point is that it takes time and can be called off.
function fetchEnrichment(uuid, signal) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => resolve({
tags: ['tlp:amber', 'malware'],
sightings: (uuid.charCodeAt(1) % 7) + 1
}), 900)
signal.addEventListener('abort', () => {
clearTimeout(timer)
reject(new DOMException('aborted', 'AbortError'))
})
})
}const data = {
nodes: [
{ id: 'attr-1', data: { label: '8.8.8.8', type: 'ip-dst', uuid: 'a1' } },
{ id: 'attr-2', data: { label: 'evil.example', type: 'domain', uuid: 'a2' } },
{ id: 'attr-3', data: { label: 'd41d8cd98f00b204', type: 'md5', uuid: 'a3' } },
{ id: 'attr-4', data: { label: 'payload.exe', type: 'filename', uuid: 'a4' } },
{ id: 'attr-5', data: { label: 'bad@example.com', type: 'email-src', uuid: 'a5' } }
],
edges: [
{ from: 'attr-1', to: 'attr-2', data: { label: 'resolves' } },
{ from: 'attr-2', to: 'attr-3', data: { label: 'served' } },
{ from: 'attr-3', to: 'attr-4', data: { label: 'is' } },
{ from: 'attr-2', to: 'attr-5', data: { label: 'registered by' } }
]
}Das Gleiche gilt für die Seitenleisten propertiesPanel, neighborsPanel, mainHeader und zusätzliche Panels – den vollständigen Vertrag finden Sie unter asynchroner Inhalt.