Pular para o conteúdo
Pivotick
Main Navigation Início
Primeiros passos
Configuração
Referência da API
GaleriaProjetosExperimente!

Português

English
简体中文
Français
Deutsch

Português

English
简体中文
Français
Deutsch

Aparência

Sidebar Navigation

Primeiros passos

Projetos que usam o Pivotick

Configuração

Funções de retorno

Camadas de arestas

Simplificar

Layout

Renderização

Simulação

Personalizar a interface

Barra lateral

Dica

Menu de contexto

Filtros

Legenda

Tabela de dados

Estilo da interface

Traduzir a interface

Plugins

API Pivotick

Pivôs e enriquecimento

Salvar resultados dos pivôs

Desfazer e histórico

Segurança

Documentação da API

Galeria

Fundamentos

Estilo dos nós

Estilo das arestas

Layouts

Eventos e funções de retorno

Personalização da interface

Edição e criação

Notas e anotações

Filtros, pesquisa e hierarquia

Controle por programação

Temas e desempenho

Pivôs e enriquecimento

Demonstrações completas

Persistir exclusões, edições e criações ​

Quando o gráfico é uma visão de registros persistentes, nenhuma gravação pode ser otimista. Três ganchos anteriores fazem com que cada usuário iniciado escreva uma proposta que o consumidor decida, para que o canvas e o banco de dados nunca possam discordar:

  • onBeforeDelete — dispara para cada affordance de exclusão (a linha em massa da barra lateral, as entradas do menu de contexto do nó/borda/nota) assim que todo o conjunto de destino for resolvido, incluindo cascadingEdges: os relacionamentos que a remoção de um nó destruiria. Retorne false para vetar ou { accept: true, nodes, edges } para manter apenas o que o servidor realmente excluiu. ctx.confirm() é o modal de confirmação integrado.
  • onBeforeNodeCreate — dispara para Create ▸ Add node e o menu de tela Add Node Here, com o espaço gráfico position e um formulário promptData(). O que você retorna é o que entra no gráfico.
  • onBeforeEdgeEditCommit — dispara quando uma edição de borda é confirmada (clique com o botão direito em uma borda ▸ Edit Edge). Recuse e a borda manterá seus dados.

Todos os três podem ser assíncronos e um segundo gesto é ignorado enquanto um está pendente. Os graph.removeNode() / addNode() programáticos nunca são bloqueados, portanto, seu próprio código não é bloqueado por seu próprio gancho.

Experimente: selecione nós e pressione Delete na barra lateral (o registro published volta recusado), clique com o botão direito em uma borda para alterar seu relacionamento ou use Create ▸ Add node. Defina editors.deletion.enabled: false para remover completamente as possibilidades de exclusão, em vez de vetar cada clique.

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' } }
    ]
}
Pager
Próxima páginaPrimeiros passos