ドックペインを追加する
一番下のドックは 共有領域 であり、データテーブルのプライベートプロパティではありません: 1 つのグリッド 行、高さは 1、折り目は 1 つですが、その中に含まれるペインの数は関係ありません。 addDockTab() はあなたの置き方です そこには何か別のものがあります - 内蔵テーブルが通るのと同じドアなので、窓ガラスはあなたが register はゲストではなく同等です。
このカードは、グラフを所有者ごとまたは種類ごとにロールアップする 概要 ペインを追加します。ドックの ストリップにはTable │ Summaryと書かれています。横をクリックすると本文 * と * ヘッダー コントロールが変わります Select all / CSV / Columns はテーブルに属しており、何も意味しないためです。 ロールアップの上に。
1 つのタブが 1 つのペインになります
[概要] ペインには、所有者別 および 種類別 の 2 つの方法でグラフが表示されます。 2 つのペインではなく、1 つのペインのビュー。したがって、single ドック タブを登録し、独自のタブを描画します 切り替えて、refresh() を呼び出して本体を変更します。
その違いがデザイン全体です。 2 つのビューを 2 つのドック タブとして登録すると、 それらをドックのTableの横のストリップにリストし、概要と データテーブル全体は同じ種類のものです。組み込みテーブルはまさにこれを実行します ペインの機能: Nodes と Edges は その タブであり、独自のバーに描画されます。
2 つのレベルは別々に描画されているため、隣り合っても次のように読むことができます。 外側と内側:
| 見てください | クラス | |
|---|---|---|
Table │ Summary | フルハイトのタブ、アクティブな場合は下線が引かれ、ルールで閉じられます | pvt-dock-tabs / pvt-dock-tab |
Nodes │ Edges, By owner │ By kind | 小さな錠剤グループ | pvt-dock-views / pvt-dock-view |
内側のペアはパブリックであるため、以下のスイッチは 1 つのクラス名ではなく 3 つのクラス名になっています。 インライン スタイルのブロック: ペインは組み込みテーブルと同じコントロールを取得し、 --pvt-theme-primary を retint するか、いずれかのクラスをオーバーライドする消費者は、両方のレベルを retint します。 すぐに。
refresh() は省略可能な丁寧さではありません
ドック ** は、render が返した要素** を保持し、ペインが閉じられたときにそれを再アタッチします。 前に戻ってきます。独自の DOM を交換したペインは、ドックに 後で返却する古いノード。したがって、身体の変化は refresh() を通過します。 またrender。
誰も見ていないときを知る
onActivate / onDeactivate は、ペインが画面外にあることを示す唯一の信号です。 それらをどうするかはあなた次第です:
- グラフの現在の状態の関数であるコンテンツは、実行中に動作を停止する可能性があります。 非表示にして、戻ったときに再派生します。それがこのペインの役割であり、データ テーブルの役割です。 そうです。
- 何かを 見逃し にするコンテンツは機能し続け、単に描画を停止する必要があります — たとえば、イベントが発生するとイベントは消滅するため、ライブバスを監視するペインです。
フックについても好みのものはありません。
ドックも一緒に持ち運べる
プラグインは * UI が構築された後に * をインストールするため、ドック タブは常にリージョンの後に到着します。 独自モードゲートが動作しました。したがって、登録すると ** がドックに ** を構築します。プラグインは機能します。 UI.table: false を使用しており、full モード以外は何もオンにする必要はありません。テーブルを外した状態で、 独自の 1 つのペインであり、ストリップはまったく描画されません。ドックは単にあなたのペインです。
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' } },
],
}