Doorgaan met verwijderen, bewerken en maken
Wanneer de grafiek een weergave van persistente records is, kan geen enkele schrijfwijze optimistisch zijn. Drie before-hooks zorgen ervoor dat elke door de gebruiker geïnitieerde tekst een voorstel schrijft waar de consument over beslist, dus het canvas en de database kunnen het nooit oneens zijn:
onBeforeDelete— wordt geactiveerd voor elke verwijdermogelijkheid (de bulkrij van de zijbalk, de node / edge / note contextmenu-items) zodra de hele doelset is opgelost, inclusiefcascadingEdges: de relaties die het verwijderen van een knooppunt zou vernietigen. Geeffalseterug om een veto uit te spreken, of{ accept: true, nodes, edges }om alleen te behouden wat de server daadwerkelijk verwijderd.ctx.confirm()is het ingebouwde bevestigingsmodaliteit.onBeforeNodeCreate— wordt geactiveerd voor Maken ▸ Knooppunt en de canvasmenu's toevoegen Voeg hier een knooppunt toe, met de grafiekruimtepositionen eenpromptData()-vorm. Wat wat u teruggeeft, is wat in de grafiek terechtkomt.onBeforeEdgeEditCommit— wordt geactiveerd wanneer een randbewerking wordt uitgevoerd (klik met de rechtermuisknop op een rand ▸ Rand bewerken). Weiger het en de edge behoudt zijn gegevens.
Alle drie kunnen asynchroon zijn en een tweede gebaar wordt genegeerd terwijl er één in behandeling is. Programmatic graph.removeNode() / addNode() zijn nooit beveiligd, dus uw eigen code wordt niet geblokkeerd door je eigen hook.
Probeer het: selecteer knooppunten en druk op Delete in de zijbalk (het record gepubliceerd verschijnt terug geweigerd), klik met de rechtermuisknop op een rand om de relatie ervan te wijzigen, of gebruik Maken ▸ Toevoegen knooppunt. Stel editors.deletion.enabled: false in om de verwijdermogelijkheden te verwijderen helemaal in plaats van elke klik te vetoën.
const options = {
// Label each attribute with its value, so an edit's effect is visible.
render: {
defaultNodeStyle: {
size: 16,
textColor: '#334155',
text: (node) => node.getData()?.label,
textVerticalShift: -1.6
}
},
UI: {
mode: 'full',
// Off: `full` mode brings a minimap and the data dock, and this card is about
// neither (see the Minimap and Data table cards for those).
minimap: false,
table: false,
// Give the edge editor a relationship dropdown instead of a free-text field.
editors: {
edgeEditor: {
fields: [{
key: 'label',
label: 'Relationship',
type: 'select',
options: RELATIONSHIPS.map((value) => ({ value, label: value }))
}]
}
}
},
callbacks: {
// Nothing leaves the model until the server agrees. The library resolves the
// whole target set first — including `cascadingEdges`, the relationships that
// removing a node would take with it — so you can persist the real consequence.
onBeforeDelete: async ({ nodes, edges, cascadingEdges, origin, confirm }) => {
const links = edges.length + cascadingEdges.length
const ok = await confirm({
title: 'Delete from event?',
body: `${nodes.length} attribute(s) and ${links} relationship(s) will be `
+ `deleted server-side. (asked from the ${origin})`,
confirmLabel: 'Delete'
})
if (!ok) return false
graph.notifier.info('Saving…', 'POST /events/delete')
await wait(700) // a backend call that could fail
// The server refused the published record: keep exactly what it deleted.
const refused = nodes.filter((node) => node.getData().published)
if (refused.length) {
graph.notifier.warning('Kept', `${refused[0].getData().label} is published — not deleted.`)
}
return { accept: true, nodes: nodes.filter((node) => !node.getData().published) }
},
// Create ▸ Add node, or the canvas menu's "Add Node Here". `promptData` opens a
// form; the id/data you return is what enters the graph.
onBeforeNodeCreate: async ({ position, promptData }) => {
const values = await promptData({
title: 'New attribute',
fields: [
{ key: 'label', label: 'Value', type: 'text' },
{
key: 'type', label: 'Type', type: 'select', defaultValue: 'ip-src',
options: ['ip-src', 'domain', 'md5', 'url'].map((v) => ({ value: v, label: v }))
}
]
})
if (!values) return false // cancelled
graph.notifier.info('Saving…', 'POST /attributes/add')
await wait(500)
graph.notifier.success('Created', `${values.label} at ${Math.round(position.x)},${Math.round(position.y)}`)
return { accept: true, id: `attr-${values.label}`, data: values }
},
// Right-click an edge ▸ Edit Edge. The commit is a proposal too: refuse it and
// the edge keeps its data, with the modal still open to correct.
onBeforeEdgeEditCommit: async ({ previousData, nextData }) => {
if (!RELATIONSHIPS.includes(nextData.label)) {
graph.notifier.warning('Rejected', `"${nextData.label}" is not a known relationship.`)
return false
}
graph.notifier.info('Saving…', 'PUT /object_references')
await wait(500)
graph.notifier.success('Updated', `${previousData.label} → ${nextData.label}`)
return true
}
}
}const data = {
nodes: [
{ id: 'ip-8888', data: { label: '8.8.8.8', type: 'ip-src' } },
{ id: 'dom-evil', data: { label: 'evil.example.com', type: 'domain' } },
{ id: 'md5', data: { label: 'd41d8cd9…f8427e', type: 'md5' } },
// The server refuses to delete this one — a published record.
{ id: 'file-obj', data: { label: 'invoice.doc', type: 'file', published: true } }
],
edges: [
{ id: 'r1', from: 'file-obj', to: 'md5', data: { label: 'contains' } },
{ id: 'r2', from: 'dom-evil', to: 'ip-8888', data: { label: 'resolves-to' } }
]
}