Voeg een dockvenster toe
Het onderste dock is een gedeelde regio, niet het privé-eigendom van de gegevenstabel: één raster rij, één hoogte, één vouw, hoeveel ruiten er ook in zitten. addDockTab() is hoe je het zegt er zit nog iets anders in - dezelfde deur waar de ingebouwde tafel doorheen komt, dus een ruit register is zijn gelijke in plaats van zijn gast.
Deze kaart voegt een deelvenster Samenvatting toe waarin de grafiek wordt weergegeven per eigenaar of per soort. Van het dok strip leest Table │ Summary; klik aan de overkant en de hoofdtekst en en de header-besturingselementen veranderen ermee, omdat Select all / CSV / Columns tot de tabel behoren en niets betekenen boven een roll-up.
Eén tabblad is één venster
In het deelvenster Samenvatting wordt de grafiek op twee manieren weergegeven — Door eigenaar en Per soort — en dat zijn weergaven van één venster, niet twee vensters. Het registreert dus een enkel docktabblad en tekent zijn eigen schakelaar, bel refresh() om van lichaam te veranderen.
Dat onderscheid is het hele ontwerp. Het registreren van de twee weergaven zoals twee docktabbladen dat zouden doen noteer ze in de dokstrook naast Table, waarbij je beweert dat een weergave van de samenvatting en de hele gegevenstabel is hetzelfde. De inbouwtafel doet precies wat dit doet paneel doet: Nodes en Edges zijn zijn tabbladen, getekend op een eigen balk.
De twee niveaus zijn anders getekend, zodat ze naast elkaar kunnen zitten en toch kunnen lezen een buitenste en een binnenste:
| Kijk | Klasse | |
|---|---|---|
Table │ Summary | tabbladen op volledige hoogte, onderstreept indien actief, afgesloten door een regel | pvt-dock-tabs / pvt-dock-tab |
Nodes │ Edges, By owner │ By kind | een kleine pillengroep | pvt-dock-views / pvt-dock-view |
Het binnenste paar is openbaar. Daarom bestaat de onderstaande schakelaar uit drie klassennamen in plaats van a blok met inline-stijl: uw paneel krijgt dezelfde controle als de ingebouwde tabel, en a consument die --pvt-theme-primary opnieuw kleurt of een van beide klassen overschrijft, verandert beide niveaus tegelijk.
refresh() is geen optionele beleefdheid
Het dock bewaart het element dat uw render teruggeeft en koppelt het opnieuw wanneer uw venster komt weer naar voren. Een paneel dat zijn eigen DOM verwisselde, zou het dok verlaten met een oud knooppunt om later terug te geven. Het wisselen van lichaam gaat dus via refresh(), wat roept render opnieuw.
Weten wanneer niemand kijkt
onActivate / onDeactivate zijn het enige signaal dat een paneel krijgt dat het buiten beeld is, en wat je ermee doet, is aan jou om te kiezen:
- Inhoud die een functie is van de huidige status van de grafiek kan stoppen met werken terwijl verborgen en bij terugkomst opnieuw ontlenen. Dat is wat dit venster doet, en wat de gegevenstabel doet doet.
- Inhoud die missen iets zou moeten blijven werken en alleen maar stoppen met schilderen — een ruit die bijvoorbeeld naar een rijdende bus kijkt, aangezien een gebeurtenis voorbij is zodra deze is afgevuurd.
Ook niets over de haken heeft de voorkeur.
Het brengt het dok met zich mee
Plug-ins worden geïnstalleerd na de gebruikersinterface is gebouwd, zodat een docktabblad altijd na de regio arriveert eigen moduspoort is uitgevoerd. Als u er een registreert, wordt daarom het dock gebouwd — uw plug-in werkt met UI.table: false en heeft niets anders nodig dan de full-modus. Met de tafel af en één ruitje voor jezelf, er wordt helemaal geen strook getekend: de dock is gewoon jouw ruitje.
UI.dock configureert de regio zelf (open, collapsed, height) — reik ernaar in plaats van UI.table als de tafel uitgeschakeld is, aangezien dat dan de enige deur is.
Zie Plug-ins voor het contract en Gegevenstabel voor de eigen opties van het dock.
// 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' } },
],
}