Fügen Sie einen Dockbereich hinzu
Das untere Dock ist ein gemeinsamer Bereich, nicht das Privateigentum der Datentabelle: eine Rasterzeile, eine Höhe, eine Falte, egal wie viele Bereiche darin enthalten sind. addDockTab() ist die Art und Weise, wie Sie etwas anderes dort hineinstellen – die gleiche Tür, durch die der Einbautisch kommt, sodass ein Fenster, das Sie registrieren, ihm gleichgestellt und nicht sein Gast ist.
Diese Karte fügt einen Bereich Zusammenfassung hinzu, der das Diagramm nach Eigentümer oder Art aufrollt. Auf dem Streifen des Docks steht Table │ Summary; Klicken Sie rüber und der Körper und ändert sich mit ihm, denn Select all / CSV / Columns gehören zur Tabelle und bedeuten nichts über ein Roll-Up.
Eine Registerkarte ist ein Bereich
Der Zusammenfassungsbereich zeigt das Diagramm auf zwei Arten an – Nach Besitzer und Nach Art – und das sind Ansichten eines Bereichs, nicht zwei Bereiche. Es registriert also eine einzelne Dock-Registerkarte und zeichnet seinen eigenen Schalter, der refresh() aufruft, um den Körper zu ändern.
Diese Unterscheidung ist das gesamte Design. Wenn Sie die beiden Ansichten als zwei Dock-Registerkarten registrieren, werden sie in der Leiste des Docks neben Table aufgeführt und behaupten, dass eine Ansicht der Zusammenfassung und die gesamte Datentabelle dasselbe sind. Die integrierte Tabelle macht genau das, was dieser Bereich macht: Nodes und Edges sind ihre Registerkarten, die in einer eigenen Leiste gezeichnet werden.
Die beiden Ebenen sind unterschiedlich gezeichnet, sodass sie nebeneinander liegen und dennoch als äußere und innere Ebene gelesen werden können:
| Schauen Sie | Klasse | |
|---|---|---|
Table │ Summary | Tabulatoren in voller Höhe, unterstrichen, wenn sie aktiv sind, und durch eine Regel geschlossen | pvt-dock-tabs / pvt-dock-tab |
Nodes │ Edges, By owner │ By kind | eine kleine Pillengruppe | pvt-dock-views / pvt-dock-view |
Das innere Paar ist öffentlich, weshalb der Schalter unten aus drei Klassennamen und nicht aus einem Block mit Inline-Stil besteht: Ihr Bereich erhält die gleiche Kontrolle wie die integrierte Tabelle, und ein Verbraucher, der --pvt-theme-primary neu formatiert oder eine der Klassen überschreibt, ändert beide Ebenen gleichzeitig neu.
refresh() ist keine optionale Höflichkeit
Das Dock behält das von Ihnen render zurückgegebene Element und fügt es erneut hinzu, wenn Ihr Fenster wieder im Vordergrund erscheint. Ein Bereich, der sein eigenes DOM ausgetauscht hat, würde das Dock mit einem veralteten Knoten verlassen, um ihn später zurückzugeben. Die Änderung des Körpers durchläuft also refresh(), was wiederum render aufruft.
Wissen, wenn niemand hinschaut
onActivate / onDeactivate sind das einzige Signal, das ein Bereich erhält, dass er sich außerhalb des Bildschirms befindet, und was Sie damit machen möchten, liegt bei Ihnen:
- Inhalte, die eine Funktion des aktuellen Zustands des Diagramms sind, können im ausgeblendeten Zustand nicht mehr funktionieren und bei der Rückkehr erneut abgeleitet werden. Das ist es, was dieser Bereich und die Datentabelle tun.
- Inhalte, die etwas verpassen würden, müssen weiter funktionieren und lediglich aufhören zu malen – beispielsweise ein Fenster, das einen Live-Bus beobachtet, da ein Ereignis verschwunden ist, sobald es ausgelöst wurde.
An den Haken gefällt mir auch nichts.
Es bringt das Dock mit
Plugins werden nach der Erstellung der Benutzeroberfläche installiert, sodass immer eine Dock-Registerkarte angezeigt wird, nachdem das eigene Modus-Gate der Region ausgeführt wurde. Wenn Sie eines registrieren, wird daher das Dock erstellt – Ihr Plugin funktioniert mit UI.table: false und erfordert nichts anderes als den full-Modus. Bei ausgeschaltetem Tisch und einem eigenen Bereich wird überhaupt kein Streifen gezeichnet: Das Dock ist einfach Ihr Bereich.
UI.dock konfiguriert die Region selbst (open, collapsed, height) – greifen Sie danach und nicht nach UI.table, wenn der Tisch ausgeschaltet ist, da dies dann die einzige Tür ist.
Siehe Plugins für den Vertrag und Datentabelle für die eigenen Optionen des Docks.
// The bottom dock is a shared region: one row, one height, one fold, however many panes
// are in it. `addDockTab` is the door — the same one the built-in data table comes
// through, so a pane you register is its equal rather than its guest.
//
// **One tab is one pane, not one view of one.** This pane rolls the graph up two ways,
// by owner and by kind. Those are two views of *this* pane, so it registers a single
// dock tab and draws its own switch, calling `refresh()` to change body. Registering
// them as two dock tabs would put them in the dock's strip beside `Table`, claiming a
// view of the summary and the whole data table are the same kind of thing.
//
// That is also why the two levels look different: the dock draws panes as full-height
// underlined tabs, and a pane's own views a lighter pill group. That inner look is public
// — `pvt-dock-views` on the strip, `pvt-dock-view` on each button, `active` on the current
// one — so your pane matches the built-in table for free, and follows the theme when a
// consumer overrides `--pvt-theme-primary` or either class.
const VIEWS = [
{ key: 'owner', label: 'By owner' },
{ key: 'kind', label: 'By kind' },
]
/** Count the nodes under each value of `key`, biggest group first. */
function rollUp(graph, key) {
const counts = new Map()
for (const node of graph.getNodes()) {
const value = node.getData()?.[key] ?? '—'
counts.set(value, (counts.get(value) ?? 0) + 1)
}
return [...counts.entries()].sort((a, b) => b[1] - a[1])
}
/**
* The pane's body. `render` is called once per pane, lazily, the first time it is
* opened — and again on every `refresh()`, which is how the view switch works.
*/
function renderSummary(graph, key) {
const rows = rollUp(graph, key)
const biggest = Math.max(...rows.map(([, count]) => count), 1)
const body = document.createElement('div')
body.style.cssText = 'padding: 8px 12px; font: 12px/1.9 sans-serif'
for (const [value, count] of rows) {
const row = document.createElement('div')
row.style.cssText = 'display: grid; grid-template-columns: 130px 1fr 34px; gap: 10px; align-items: center'
const name = document.createElement('span')
name.textContent = value
// A bar rather than a number alone: the shape of the split is the point.
const track = document.createElement('span')
track.style.cssText = 'height: 7px; border-radius: 4px; background: var(--pvt-bg-color-4)'
const fill = document.createElement('span')
fill.style.cssText = `display: block; height: 100%; border-radius: 4px; background: var(--pvt-vibrant-blue); width: ${(count / biggest) * 100}%`
track.appendChild(fill)
const tally = document.createElement('span')
tally.textContent = String(count)
tally.style.cssText = 'text-align: right; color: var(--pvt-text-color-3)'
row.append(name, track, tally)
body.appendChild(row)
}
return body
}
/**
* The pane's own controls, in the header slot the dock hands it. `toolbar` is re-invoked
* on every activation, so these always read the pane's current state.
*/
function renderControls(current, pick) {
const strip = document.createElement('div')
strip.className = 'pvt-dock-views'
for (const view of VIEWS) {
const button = document.createElement('button')
button.type = 'button'
button.className = 'pvt-dock-view'
button.textContent = view.label
button.classList.toggle('active', view.key === current)
button.setAttribute('aria-pressed', String(view.key === current))
button.addEventListener('click', () => pick(view.key))
strip.appendChild(button)
}
return strip
}
/**
* Ships as a plugin, so it installs itself and the core never needs to know it exists.
* A plugin's tab also *builds* the dock if there isn't one — plugins install after the
* UI, so it always arrives after the region's own gate has run.
*/
function summaryPane() {
return {
name: 'summaryPane',
install(ctx) {
let view = 'owner'
// Whether the pane is the one on show, and whether the graph moved while it
// wasn't. `onActivate` / `onDeactivate` are the only signal you get, and this
// is the shape most panes want: don't work while hidden, catch up on return.
let visible = false
let stale = false
ctx.addDockTab({
id: 'summary',
label: 'Summary',
render: () => renderSummary(ctx.graph, view),
toolbar: (pane) => renderControls(view, (key) => {
view = key
// The dock keeps the element `render` gave it, so ask it to rebuild
// rather than swapping the DOM behind its back.
pane.refresh()
}),
onActivate: (pane) => {
visible = true
if (stale) { stale = false; pane.refresh() }
},
onDeactivate: () => { visible = false },
})
// The graph outlives nothing here, but a real plugin should keep the
// unsubscribe — see the Extend with a plugin card.
ctx.graph.on('dataBatchChanged', () => {
if (visible) ctx.refreshDockTab('summary')
else stale = true
})
},
}
}
const options = {
UI: {
mode: 'full',
// The dock is the subject, so it starts expanded; the table is the pane our own
// one sits beside, and is what makes the strip appear at all.
dock: { open: true, height: 0.4 },
table: { columns: [
{ key: 'label', label: 'Service', type: 'text' },
{ key: 'kind', label: 'Kind', type: 'select' },
{ key: 'owner', label: 'Owner', type: 'select' },
] },
// Full mode's minimap isn't what this card is about.
minimap: false,
},
plugins: [summaryPane()],
}const data = {
nodes: [
{ id: 'api', data: { label: 'api-gateway', kind: 'service', owner: 'Platform' } },
{ id: 'auth', data: { label: 'auth-service', kind: 'service', owner: 'Identity' } },
{ id: 'billing', data: { label: 'billing-service', kind: 'service', owner: 'Payments' } },
{ id: 'search', data: { label: 'search-service', kind: 'service', owner: 'Discovery' } },
{ id: 'users-db', data: { label: 'users-db', kind: 'datastore', owner: 'Identity' } },
{ id: 'orders-db', data: { label: 'orders-db', kind: 'datastore', owner: 'Payments' } },
{ id: 'index', data: { label: 'search-index', kind: 'datastore', owner: 'Discovery' } },
{ id: 'cache', data: { label: 'edge-cache', kind: 'datastore', owner: 'Platform' } },
{ id: 'mailer', data: { label: 'mailer', kind: 'worker', owner: 'Payments' } },
{ id: 'reindexer', data: { label: 'reindexer', kind: 'worker', owner: 'Discovery' } },
],
edges: [
{ from: 'api', to: 'auth', data: { label: 'authenticates' } },
{ from: 'api', to: 'search', data: { label: 'queries' } },
{ from: 'api', to: 'billing', data: { label: 'charges' } },
{ from: 'api', to: 'cache', data: { label: 'reads' } },
{ from: 'auth', to: 'users-db', data: { label: 'reads' } },
{ from: 'billing', to: 'orders-db', data: { label: 'writes' } },
{ from: 'billing', to: 'mailer', data: { label: 'enqueues' } },
{ from: 'search', to: 'index', data: { label: 'reads' } },
{ from: 'reindexer', to: 'index', data: { label: 'writes' } },
{ from: 'reindexer', to: 'orders-db', data: { label: 'reads' } },
],
}