Conteúdo assíncrono da dica de ferramenta
Quando os dados de um nó são uma referência para um registro em vez do registro em si, o conteúdo interessante está por trás de uma chamada HTTP. Retorne uma promise de um gancho de conteúdo e a biblioteca cuida do resto: ela mostra um espaço reservado, troca o conteúdo quando é resolvido e descarta o resultado se a dica de ferramenta tiver sido movida para outro nó.
Passe o mouse sobre um nó e espere – os chips de enriquecimento chegam após aproximadamente 900 ms. Em seguida, passe o mouse sobre um nó e passe imediatamente para o próximo: a solicitação abandonada é abortada via ctx.signal e seu resultado tardio nunca chega à dica de ferramenta que você está vendo.
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' } }
]
}O mesmo se aplica aos painéis propertiesPanel, neighborsPanel, mainHeader e extras da barra lateral — consulte conteúdo assíncrono para o contrato completo.