الاستمرار في الحذف والتعديل والإنشاء
عندما يكون الرسم البياني عبارة عن عرض للسجلات المستمرة، لا يمكن لأي كتابة أن تكون متفائلة. ثلاثة الخطافات المسبقة تجعل كل مستخدم يبدأ في كتابة اقتراح يقرره المستهلك، لذلك لا يمكن أن تختلف اللوحة القماشية وقاعدة البيانات أبدًا:
onBeforeDelete— يتم تشغيله لكل إمكانية حذف (الصف الكبير في الشريط الجانبي، إدخالات قائمة السياق للعقدة / الحافة / الملاحظة) بمجرد حل المجموعة المستهدفة بأكملها، بما في ذلكcascadingEdges: العلاقات التي قد تؤدي إزالة العقدة إلى تدميرها. العودةfalseإلى حق النقض، أو{ accept: true, nodes, edges }للاحتفاظ فقط بما تم حذف الخادم بالفعل.ctx.confirm()هو نموذج التأكيد المدمج.onBeforeNodeCreate— يتم تشغيله لـ إنشاء ▸ إضافة العقدة وقائمة القماش أضف عقدة هنا، مع مساحة الرسم البيانيpositionونموذجpromptData(). ماذا عدت هو ما يدخل الرسم البياني.onBeforeEdgeEditCommit— يتم تشغيله عند إجراء تعديل على الحافة (انقر بزر الماوس الأيمن فوق الحافة ▸ تحرير الحافة). ارفضها وستحتفظ الحافة ببياناتها.
قد تكون الثلاثة جميعها غير متزامنة، ويتم تجاهل الإيماءة الثانية بينما تكون إحداها معلقة. البرامج graph.removeNode() / addNode() لا تكون مسورة أبدًا، لذا فإن الكود الخاص بك لا يتم حظره بواسطة الخطاف الخاص بك.
جربه: حدد العقد واضغط على حذف في الشريط الجانبي (يأتي السجل منشور تم رفض الرجوع)، انقر بزر الماوس الأيمن على إحدى الحواف لتغيير علاقتها، أو استخدم إنشاء ▸ إضافة العقدة. اضبط editors.deletion.enabled: false لإزالة إمكانيات الحذف تمامًا بدلاً من الاعتراض على كل نقرة.
js
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
}
}
}js
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' } }
]
}