Ajouter un mode ferroviaire
Le rail de gauche contient les modes, pas les boutons. La sélection, la création, l'affichage et la physique sont intégrés et chacun n'affiche que ses propres outils. C'est pourquoi le rail reste haut de quatre emplacements, quelle que soit la capacité du graphique. addRailMode() en ajoute un cinquième.
Cette carte ajoute un mode Explore pour parcourir un graphe de service : choisissez un nœud, sélectionnez ses voisins, réduisez le canevas à eux, puis revenez en arrière. Appuyez sur E ou cliquez sur la boussole sous le séparateur.
Où atterrit votre mode
Les modes enregistrés se trouvent sous un diviseur, après les modes intégrés, classés entre eux par order. Un plugin ne peut pas réorganiser ou supprimer les quatre intégrés, donc tout ce qui se trouve au-dessus de la ligne est le même dans chaque graphique Pivotick, et tout ce qui se trouve en dessous vous appartient.
Déclarez les outils, pas le balisage
Les tools d'un mode deviennent des lignes dans le panneau contextuel, dessinées comme celles de Select et Create. Vous fournissez les verbes ; le kind de chaque ligne indique comment elle se comporte :
kind | Comportement |
|---|---|
'action' | s'exécute une fois et laisse le mode seul - les trois outils ici |
'toggle' | arme un outil : le panneau s'effondre et le bouton du rail devient cet outil, de la même manière que l'emplacement de Select devient Lasso |
'default' | l'outil sur lequel repose le mode et à quoi revient une bascule |
enabled() est revérifié chaque fois que la sélection change — c'est pourquoi Sélectionnez les voisins et Isolate restent grisés jusqu'à ce que vous choisissiez un nœud.
Rien ici n'est privilégié
Chaque outil ci-dessus appelle une API publique que vous pourriez appeler vous-même : getEdges() pour parcourir les relations, selectNodes() pour les sélectionner, setVisibleNodes() pour affiner le canevas. Le mode leur donne simplement un endroit où vivre : un emplacement ferroviaire, un raccourci, un état armé et un panneau.
Quel est le point : un mode Explorer qui sait ce que "voisin" signifie pour vos données est quelque chose que vous pouvez créer et nous ne pouvons pas.
Les modes s'excluent
L'ouverture d'Explore ferme la vue et la physique, et vice-versa : un magasin, un mode actif. Annulez quel que soit votre mode armé en onExit ; il s'exécute également si le mode est supprimé alors qu'il est actif et que le rail revient à Select.
Quand un flyout s'adapte mieux
Un mode qui configure plutôt qu’agit souhaite une large superposition, et non une bande d’outils étroite. Définissez kind: 'flyout', sous-classez le Flyout exporté et transmettez addRailMode une usine — voir Contribuer à un mode ferroviaire.
// The rail's four modes — Select, Create, View, Physics — are built in. `addRailMode`
// is how you add a fifth. It renders below a divider, after them, and drives the same
// contextual tool panel the built-ins do: declare `tools`, and the arming, the
// enabled/disabled states and the collapse are handled for you.
//
// This card adds an **Explore** mode for walking the graph: reach the neighbours of what
// is selected, isolate a subgraph, and step back out. Nothing here is privileged — every
// tool is public graph API a consumer could call themselves.
const compass = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20"><path fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" d="M12 21a9 9 0 1 0 0-18a9 9 0 0 0 0 18Z"/><path fill="currentColor" d="M15.5 8.5l-2 5l-5 2l2-5z"/></svg>'
const expandIcon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M11 13v3a1 1 0 0 0 2 0v-3h3a1 1 0 0 0 0-2h-3V8a1 1 0 0 0-2 0v3H8a1 1 0 0 0 0 2zm1 9a10 10 0 1 1 0-20a10 10 0 0 1 0 20m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16"/></svg>'
const isolateIcon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="18" height="18" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="M16 6H8a2 2 0 0 0-2 2v8m10 26H8a2 2 0 0 1-2-2v-8m26 10h8a2 2 0 0 0 2-2v-8M32 6h8a2 2 0 0 1 2 2v8"/><circle cx="24" cy="24" r="6"/></svg>'
const resetIcon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 5V2L8 6l4 4V7a5 5 0 1 1-5 5H5a7 7 0 1 0 7-7"/></svg>'
function exploreMode() {
return {
name: 'explore-mode',
install(ctx) {
const interaction = () => ctx.graph.renderer.getGraphInteraction()
const selected = () => interaction().getSelectedNode()?.node
/** Every node one hop from `id`, itself included. */
const withNeighbours = (id) => {
const keep = new Set([id])
for (const edge of ctx.graph.getEdges()) {
if (edge.from.id === id) keep.add(edge.to.id)
if (edge.to.id === id) keep.add(edge.from.id)
}
return keep
}
ctx.addRailMode({
id: 'explore',
label: 'Explore',
icon: compass,
// Pressing E switches to the mode, or toggles its panel when it is
// already active — exactly what V and C do for Select and Create.
shortcut: 'E',
tools: [
{
// A one-shot action: it runs and leaves the mode as it was.
id: 'expand',
label: 'Select neighbours',
icon: expandIcon,
kind: 'action',
// Re-checked on every selection change, so the row greys out
// when it would do nothing.
enabled: () => !!selected(),
run: () => {
const node = selected()
if (!node) return
const keep = withNeighbours(node.id)
interaction().selectNodes(
ctx.graph.getMutableNodes()
.filter(n => keep.has(n.id))
.map(n => ({ node: n, element: n.getGraphElement() }))
)
},
},
{
id: 'isolate',
label: 'Isolate neighbourhood',
icon: isolateIcon,
kind: 'action',
enabled: () => !!selected(),
run: () => {
const node = selected()
if (!node) return
const keep = withNeighbours(node.id)
ctx.graph.setVisibleNodes(
ctx.graph.getMutableNodes().filter(n => keep.has(n.id))
)
},
},
{
id: 'reset',
label: 'Show everything',
icon: resetIcon,
kind: 'action',
run: () => ctx.graph.setVisibleNodes(ctx.graph.getMutableNodes()),
},
],
})
},
}
}
const options = {
UI: {
mode: 'full',
// Neither is what this card is about, and both crowd a small canvas.
minimap: false,
table: false,
},
plugins: [exploreMode()],
}const data = {
nodes: [
{ id: 'api', data: { label: 'api-gateway', kind: 'service' } },
{ id: 'auth', data: { label: 'auth-service', kind: 'service' } },
{ id: 'billing', data: { label: 'billing-service', kind: 'service' } },
{ id: 'search', data: { label: 'search-service', kind: 'service' } },
{ id: 'users-db', data: { label: 'users-db', kind: 'datastore' } },
{ id: 'orders-db', data: { label: 'orders-db', kind: 'datastore' } },
{ id: 'index', data: { label: 'search-index', kind: 'datastore' } },
{ id: 'cache', data: { label: 'edge-cache', kind: 'datastore' } },
{ id: 'mailer', data: { label: 'mailer', kind: 'worker' } },
{ id: 'reindexer', data: { label: 'reindexer', kind: 'worker' } },
],
edges: [
{ from: 'api', to: 'auth' },
{ from: 'api', to: 'search' },
{ from: 'api', to: 'billing' },
{ from: 'api', to: 'cache' },
{ from: 'auth', to: 'users-db' },
{ from: 'billing', to: 'orders-db' },
{ from: 'billing', to: 'mailer' },
{ from: 'search', to: 'index' },
{ from: 'reindexer', to: 'index' },
{ from: 'reindexer', to: 'orders-db' },
],
}