Persister les suppressions, les modifications et les créations
Lorsque le graphique est une vue des enregistrements persistants, aucune écriture ne peut être optimiste. Trois crochets avant obligent chaque utilisateur à rédiger une proposition sur laquelle le consommateur décide, de sorte que le canevas et la base de données ne puissent jamais être en désaccord :
onBeforeDelete— se déclenche pour chaque possibilité de suppression (la ligne groupée de la barre latérale, les entrées du menu contextuel nœud/bord/note) une fois que l'ensemble de la cible est résolu, y compriscascadingEdges: les relations que la suppression d'un nœud détruirait. Renvoyezfalsepour veto, ou{ accept: true, nodes, edges }pour conserver uniquement ce que le serveur a réellement supprimé.ctx.confirm()est le modal de confirmation intégré.onBeforeNodeCreate— se déclenche pour Créer ▸ Ajouter un nœud et Ajouter un nœud ici du menu de canevas, avec l'espace graphiquepositionet un formulairepromptData(). Ce que vous retournez est ce qui entre dans le graphique.onBeforeEdgeEditCommit— se déclenche lorsqu'une modification de bord est validée (cliquez avec le bouton droit sur un bord ▸ Modifier le bord). Refusez-le et le Edge conserve ses données.
Tous les trois peuvent être asynchrones et un deuxième geste est ignoré lorsqu'un autre est en attente. Les programmatiques graph.removeNode() / addNode() ne sont jamais bloqués, donc votre propre code n'est pas bloqué par votre propre hook.
Essayez-le : sélectionnez des nœuds et appuyez sur Supprimer dans la barre latérale (l'enregistrement publié revient refusé), cliquez avec le bouton droit sur un bord pour modifier sa relation, ou utilisez Créer ▸ Ajouter un nœud. Définissez editors.deletion.enabled: false pour supprimer complètement les possibilités de suppression au lieu d'opposer votre veto à chaque clic.
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' } }
]
}