perf(dashboard): lazy-load DoI tiers on Repositories page
Page now renders in ~200ms. DoI badges and KPI card show a spinner while the background fetch resolves (~6s), then update reactively via Observable Mutable pattern (doiData / doiLoading). Fast path: repos, SBOM, domains, workstreams — immediate render. Slow path: /repos/doi/summary — background, non-blocking. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,20 +7,32 @@ import {API} from "./components/config.js";
|
|||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
let _repos = [], _domains = [], _sbom = [], _eps = [], _tds = [], _workstreams = [], _doi = [];
|
// Fast data — page renders as soon as this resolves (~200ms)
|
||||||
|
let _repos = [], _domains = [], _sbom = [], _eps = [], _tds = [], _workstreams = [];
|
||||||
try {
|
try {
|
||||||
[_repos, _domains, _sbom, _eps, _tds, _workstreams, _doi] = await Promise.all([
|
[_repos, _domains, _sbom, _eps, _tds, _workstreams] = await Promise.all([
|
||||||
fetch(`${API}/repos/`).then(r => r.ok ? r.json() : []),
|
fetch(`${API}/repos/`).then(r => r.ok ? r.json() : []),
|
||||||
fetch(`${API}/domains/`).then(r => r.ok ? r.json() : []),
|
fetch(`${API}/domains/`).then(r => r.ok ? r.json() : []),
|
||||||
fetch(`${API}/sbom/`).then(r => r.ok ? r.json() : []),
|
fetch(`${API}/sbom/`).then(r => r.ok ? r.json() : []),
|
||||||
fetch(`${API}/extension-points/`).then(r => r.ok ? r.json() : []),
|
fetch(`${API}/extension-points/`).then(r => r.ok ? r.json() : []),
|
||||||
fetch(`${API}/technical-debt/`).then(r => r.ok ? r.json() : []),
|
fetch(`${API}/technical-debt/`).then(r => r.ok ? r.json() : []),
|
||||||
fetch(`${API}/workstreams/`).then(r => r.ok ? r.json() : []),
|
fetch(`${API}/workstreams/`).then(r => r.ok ? r.json() : []),
|
||||||
fetch(`${API}/repos/doi/summary`).then(r => r.ok ? r.json() : []),
|
|
||||||
]);
|
]);
|
||||||
} catch {}
|
} catch {}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// DoI data — lazy-loaded. Starts as [] so the page renders immediately.
|
||||||
|
// When the fetch resolves (~6s), doiData updates and DoI badges appear.
|
||||||
|
import {Mutable} from "observablehq:stdlib";
|
||||||
|
const doiData = Mutable([]);
|
||||||
|
const doiLoading = Mutable(true);
|
||||||
|
fetch(`${API}/repos/doi/summary`)
|
||||||
|
.then(r => r.ok ? r.json() : [])
|
||||||
|
.catch(() => [])
|
||||||
|
.then(data => { doiData.value = data; doiLoading.value = false; });
|
||||||
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const repos = _repos ?? [];
|
const repos = _repos ?? [];
|
||||||
const domains = _domains ?? [];
|
const domains = _domains ?? [];
|
||||||
@@ -28,7 +40,7 @@ const sbom = _sbom ?? [];
|
|||||||
const eps = _eps ?? [];
|
const eps = _eps ?? [];
|
||||||
const tds = _tds ?? [];
|
const tds = _tds ?? [];
|
||||||
const workstreams = _workstreams ?? [];
|
const workstreams = _workstreams ?? [];
|
||||||
const doi = _doi ?? [];
|
const doi = doiData; // reactive — updates when lazy fetch completes
|
||||||
|
|
||||||
// DoI lookups
|
// DoI lookups
|
||||||
const doiBySlug = Object.fromEntries(doi.map(d => [d.repo_slug, d]));
|
const doiBySlug = Object.fromEntries(doi.map(d => [d.repo_slug, d]));
|
||||||
@@ -121,12 +133,15 @@ const doiNoneCount = repoRows.filter(r => r._doiTier === "none").length;
|
|||||||
import {withDocHelp} from "./components/doc-overlay.js";
|
import {withDocHelp} from "./components/doc-overlay.js";
|
||||||
const _h1 = document.querySelector("#observablehq-main h1");
|
const _h1 = document.querySelector("#observablehq-main h1");
|
||||||
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/repos"); }
|
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/repos"); }
|
||||||
display(html`<p style="font-size:0.85rem;color:#6b7280;margin-top:-0.5rem;">
|
display(html`<p style="font-size:0.85rem;color:#6b7280;margin-top:-0.5rem;display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;">
|
||||||
DoI tiers: <strong style="color:#ef4444;">None</strong> →
|
<span>DoI tiers: <strong style="color:#ef4444;">None</strong> →
|
||||||
<strong style="color:#f97316;">Core</strong> →
|
<strong style="color:#f97316;">Core</strong> →
|
||||||
<strong style="color:#eab308;">Standard</strong> →
|
<strong style="color:#eab308;">Standard</strong> →
|
||||||
<strong style="color:#22c55e;">Full</strong> —
|
<strong style="color:#22c55e;">Full</strong> —
|
||||||
<a href="/policy/repo-doi" style="color:#1d4ed8;">Definition of Integrated policy ↗</a>
|
<a href="/policy/repo-doi" style="color:#1d4ed8;">Definition of Integrated policy ↗</a></span>
|
||||||
|
${doiLoading ? html`<span style="display:inline-flex;align-items:center;gap:0.35rem;color:#9ca3af;font-size:0.8rem;">
|
||||||
|
<span class="doi-spinner"></span> Loading DoI tiers…
|
||||||
|
</span>` : html`<span style="color:#16a34a;font-size:0.8rem;">✓ DoI tiers loaded</span>`}
|
||||||
</p>`);
|
</p>`);
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -155,10 +170,10 @@ display(html`<div class="kpi-row">
|
|||||||
<p class="big-num">${gapCount}</p>
|
<p class="big-num">${gapCount}</p>
|
||||||
<small>${gapCount === 0 ? "✓ All repos covered" : `⚠ ${gapCount} repo(s) not ingested`}</small>
|
<small>${gapCount === 0 ? "✓ All repos covered" : `⚠ ${gapCount} repo(s) not ingested`}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="card ${doiNoneCount > 0 ? 'card-warn' : 'card-ok'}">
|
<div class="card ${doiLoading ? '' : doiNoneCount > 0 ? 'card-warn' : 'card-ok'}">
|
||||||
<h3>DoI: Fully Integrated</h3>
|
<h3>DoI: Fully Integrated</h3>
|
||||||
<p class="big-num">${doiFullCount} / ${repoRows.length}</p>
|
<p class="big-num">${doiLoading ? html`<span class="doi-spinner" style="width:1.4rem;height:1.4rem;"></span>` : `${doiFullCount} / ${repoRows.length}`}</p>
|
||||||
<small>${doiNoneCount > 0 ? `⚠ ${doiNoneCount} at tier None` : "✓ All pass Core tier"}</small>
|
<small>${doiLoading ? "Loading…" : doiNoneCount > 0 ? `⚠ ${doiNoneCount} at tier None` : "✓ All pass Core tier"}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>`);
|
</div>`);
|
||||||
```
|
```
|
||||||
@@ -174,6 +189,7 @@ function _sbomGap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _doiBadge(tier) {
|
function _doiBadge(tier) {
|
||||||
|
if (doiLoading) return html`<span style="color:#d1d5db;font-size:0.72rem;">…</span>`;
|
||||||
const color = DOI_TIER_COLOR[tier] || "#9ca3af";
|
const color = DOI_TIER_COLOR[tier] || "#9ca3af";
|
||||||
const bg = DOI_TIER_BG[tier] || "#f9fafb";
|
const bg = DOI_TIER_BG[tier] || "#f9fafb";
|
||||||
const label = DOI_TIER_LABEL[tier] || tier;
|
const label = DOI_TIER_LABEL[tier] || tier;
|
||||||
@@ -363,4 +379,12 @@ custodian register-project --domain <slug></pre>
|
|||||||
.onboard-step strong { font-size: 0.9rem; display: block; margin-bottom: 0.3rem; }
|
.onboard-step strong { font-size: 0.9rem; display: block; margin-bottom: 0.3rem; }
|
||||||
.onboard-step pre { background: var(--theme-background); border-radius: 4px; padding: 0.4rem 0.7rem; font-size: 0.8rem; overflow-x: auto; margin: 0 0 0.35rem; }
|
.onboard-step pre { background: var(--theme-background); border-radius: 4px; padding: 0.4rem 0.7rem; font-size: 0.8rem; overflow-x: auto; margin: 0 0 0.35rem; }
|
||||||
.onboard-note { font-size: 0.82rem; color: var(--theme-foreground-muted, gray); margin: 0; line-height: 1.45; }
|
.onboard-note { font-size: 0.82rem; color: var(--theme-foreground-muted, gray); margin: 0; line-height: 1.45; }
|
||||||
|
|
||||||
|
.doi-spinner {
|
||||||
|
display: inline-block; width: 0.9rem; height: 0.9rem;
|
||||||
|
border: 2px solid #e5e7eb; border-top-color: #9ca3af;
|
||||||
|
border-radius: 50%; animation: doi-spin 0.7s linear infinite;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
@keyframes doi-spin { to { transform: rotate(360deg); } }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user