添加停靠窗格
底部停靠区是一个共享区域,不是数据表的私有属性:一个网格行,一个高度,一个折叠,无论其中有多少个窗格。 addDockTab() 是你在其中放置其他东西的方式 - 内置表格通过同一扇门,因此你注册的窗格是它的平等者而不是它的客人。
此卡添加了一个 Summary 窗格,可按所有者或按种类滚动图表。码头的条子上写着Table │ Summary;单击,正文 * 和* 标题控件也会随之更改,因为 Select all / CSV / Columns 属于表格,并且与上卷相比没有任何意义。
一个选项卡就是一个窗格
摘要窗格以两种方式显示图表 - 按所有者 和 按种类 - 这些是一个窗格*的 *视图,而不是两个窗格。因此它注册了一个 single 停靠选项卡并绘制自己的开关,调用 refresh() 来更改正文。
这种区别就是整个设计。将两个视图注册为两个停靠选项卡会将它们列在Table旁边的停靠条中,声称摘要视图和整个数据表是同一类东西。内置表格的功能与此窗格完全相同:Nodes和Edges是其选项卡,绘制在其自己的栏上。
这两个层次的绘制方式不同,因此它们可以彼此相邻,但仍然可以理解为外部和内部:
| 看 | 类 | |
|---|---|---|
Table │ Summary | 全高选项卡,活动时加下划线,按规则关闭 | pvt-dock-tabs / pvt-dock-tab |
Nodes │ Edges, By owner │ By kind | 一个小药丸组 | pvt-dock-views / pvt-dock-view |
内部对是公共的,这就是为什么下面的开关是三个类名而不是内联样式块:您的窗格获得与内置表相同的控制权,并且重新着色 --pvt-theme-primary 或覆盖任一类的使用者会同时重新着色两个级别。
refresh()不是可选的礼貌
停靠栏 ** 保留您的 render 返回的** 元素,并在您的窗格返回到前面时重新附加它。交换了自己的 DOM 的窗格将使停靠栏保留一个陈旧的节点,以便稍后交还。所以改变身体会经过refresh(),它再次调用render。
知道什么时候没有人在看
onActivate / onDeactivate 是窗格获得的唯一信号,表示它已离开屏幕,如何处理它们由您选择:
- 作为图形当前状态的函数的内容可以在隐藏时停止工作并在返回时重新导出。这就是该窗格以及数据表的作用。
- 那些会错过的内容必须继续工作,只是停止绘制——比如说,一个正在观看实时公共汽车的窗格,因为事件一旦触发就消失了。
钩子也没有什么比这更好的了。
它带来了码头
插件安装后UI 已构建,因此停靠选项卡始终在该区域自己的模式门运行后到达。因此注册一个构建码头 - 您的插件可以使用UI.table: false并且不需要打开任何东西,但是full模式。当桌子关闭并拥有一个您自己的窗格时,根本不会绘制任何条带:扩展坞只是您的窗格。
UI.dock 配置区域本身(open、collapsed、height)——当桌子关闭时,伸手去拿它而不是UI.table,因为那是唯一的门。
// 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' } },
],
}