refactor(web-ui): extract shared component primitives
Introduce components.js with barTrack, barRow, barRankList, metricPill, metricStrip, and chartHeader helpers. Migrate dashboard.js and usage.js to use these primitives, replacing 13 families of duplicated CSS (stat-list, fw-bar, token-bar, metric-pill, chart-insight, chart-header, usage-chart-total, etc.) with a unified .am-* namespace. Net: -256 lines. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,13 @@ import {
|
||||
import { clearErrorBadge } from '../palette.js';
|
||||
import { app, navigate, isRouteCurrent } from '../router.js';
|
||||
import { api } from '../api.js';
|
||||
import {
|
||||
barRankList,
|
||||
barRow,
|
||||
metricPill,
|
||||
metricStrip,
|
||||
chartHeader,
|
||||
} from '../components.js';
|
||||
|
||||
// uPlot is loaded as a global IIFE; access via window.uPlot
|
||||
/* global uPlot */
|
||||
@@ -182,7 +189,8 @@ function renderSummaryCards() {
|
||||
const totalOps = (s.runs_today || 0) + (s.tool_calls_today || 0);
|
||||
const rate = totalOps > 0 ? ((s.errors_today || 0) / totalOps * 100) : 0;
|
||||
animateCounter('dash-error-rate', rate.toFixed(1) + '%');
|
||||
errorRateEl.classList.toggle('alert', rate > 5);
|
||||
const pill = errorRateEl.closest('.am-pill');
|
||||
if (pill) pill.classList.toggle('alert', rate > 5);
|
||||
}
|
||||
|
||||
if (document.getElementById('dash-cost-per-run')) {
|
||||
@@ -287,12 +295,12 @@ function renderDashboardChartInsights() {
|
||||
}
|
||||
|
||||
const peakBucket = dashboardState.timeseries.series[stats.peakIndex];
|
||||
container.innerHTML = `
|
||||
<div class="chart-insight-pill"><span class="chart-insight-label">window total</span><strong>${escapeHTML(formatCount(stats.totalEvents))}</strong></div>
|
||||
<div class="chart-insight-pill"><span class="chart-insight-label">peak bucket</span><strong>${escapeHTML(formatCount(stats.peakTotal))}</strong><span class="chart-insight-meta">${escapeHTML(formatBucketLabel(peakBucket.ts))}</span></div>
|
||||
<div class="chart-insight-pill"><span class="chart-insight-label">mix</span><strong>${escapeHTML(formatCount(stats.totalRuns))}r / ${escapeHTML(formatCount(stats.totalTools))}t / ${escapeHTML(formatCount(stats.totalErrors))}e</strong></div>
|
||||
<div class="chart-insight-pill"><span class="chart-insight-label">bucket</span><strong>${escapeHTML(dashboardState.timeseries.bucket || '-')}</strong><span class="chart-insight-meta">${escapeHTML(String(stats.bucketCount))} points</span></div>
|
||||
`;
|
||||
container.innerHTML = metricStrip([
|
||||
{ label: 'window total', value: formatCount(stats.totalEvents), variant: 'insight' },
|
||||
{ label: 'peak bucket', value: formatCount(stats.peakTotal), meta: formatBucketLabel(peakBucket.ts), variant: 'insight' },
|
||||
{ label: 'mix', value: `${formatCount(stats.totalRuns)}r / ${formatCount(stats.totalTools)}t / ${formatCount(stats.totalErrors)}e`, variant: 'insight' },
|
||||
{ label: 'bucket', value: dashboardState.timeseries.bucket || '-', meta: stats.bucketCount + ' points', variant: 'insight' },
|
||||
], { variant: 'insights' });
|
||||
}
|
||||
|
||||
function renderDashboardChartHover(idx) {
|
||||
@@ -589,23 +597,11 @@ function renderTokenPanel() {
|
||||
<div class="token-stat-value">${escapeHTML(formatTokenCount(totalTokens))}</div>
|
||||
</div>
|
||||
<div class="token-io-bars">
|
||||
<div class="token-bar-row">
|
||||
<span class="token-bar-label">Input</span>
|
||||
<div class="token-bar-track">
|
||||
<div class="token-bar-fill input" style="width:${(inputTokens / maxIO * 100).toFixed(1)}%"></div>
|
||||
</div>
|
||||
<span class="token-bar-count">${escapeHTML(formatTokenCount(inputTokens))}</span>
|
||||
</div>
|
||||
<div class="token-bar-row">
|
||||
<span class="token-bar-label">Output</span>
|
||||
<div class="token-bar-track">
|
||||
<div class="token-bar-fill output" style="width:${(outputTokens / maxIO * 100).toFixed(1)}%"></div>
|
||||
</div>
|
||||
<span class="token-bar-count">${escapeHTML(formatTokenCount(outputTokens))}</span>
|
||||
</div>
|
||||
${barRow({ name: 'Input', count: inputTokens, countDisplay: formatTokenCount(inputTokens), max: maxIO, modifier: 'input', size: 'md' })}
|
||||
${barRow({ name: 'Output', count: outputTokens, countDisplay: formatTokenCount(outputTokens), max: maxIO, modifier: 'output', size: 'md' })}
|
||||
</div>
|
||||
<div class="token-cost-display">
|
||||
<span class="token-bar-label">Est. cost today</span>
|
||||
<span class="am-pill-label">Est. cost today</span>
|
||||
<strong>${escapeHTML(totalCost ? formatCost(totalCost) : '$0.0000')}</strong>
|
||||
</div>
|
||||
</div>
|
||||
@@ -636,18 +632,9 @@ function renderLatencyPanel() {
|
||||
container.innerHTML = `
|
||||
<div class="latency-panel">
|
||||
<div class="latency-range">
|
||||
<div class="latency-range-item">
|
||||
<span class="latency-range-label">Min</span>
|
||||
<span class="latency-range-val">${escapeHTML(formatDuration(min))}</span>
|
||||
</div>
|
||||
<div class="latency-range-item">
|
||||
<span class="latency-range-label">Avg</span>
|
||||
<span class="latency-range-val">${escapeHTML(formatDuration(avg))}</span>
|
||||
</div>
|
||||
<div class="latency-range-item">
|
||||
<span class="latency-range-label">Max</span>
|
||||
<span class="latency-range-val">${escapeHTML(formatDuration(max))}</span>
|
||||
</div>
|
||||
${metricPill({ label: 'Min', value: formatDuration(min), variant: 'range' })}
|
||||
${metricPill({ label: 'Avg', value: formatDuration(avg), variant: 'range' })}
|
||||
${metricPill({ label: 'Max', value: formatDuration(max), variant: 'range' })}
|
||||
</div>
|
||||
<div class="latency-mini-bars">
|
||||
${durSeries.map((v, i) => {
|
||||
@@ -657,7 +644,7 @@ function renderLatencyPanel() {
|
||||
return `<div class="latency-mini-bar" style="height:${pct}%" title="${escapeHTML(title)}"></div>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
<div class="latency-range-label" style="margin-top:0.5rem;font-size:0.7rem;color:var(--text-dim)">Avg run duration per bucket (${escapeHTML(ts.bucket || '-')})</div>
|
||||
<div class="am-pill-label" style="margin-top:0.5rem">Avg run duration per bucket (${escapeHTML(ts.bucket || '-')})</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -680,21 +667,17 @@ function renderFrameworkBars() {
|
||||
|
||||
const maxTotal = Math.max(...entries.map(([, s]) => s.runs + s.tools + s.errors));
|
||||
|
||||
container.innerHTML = '<div class="fw-bars">' + entries.map(([name, stats]) => {
|
||||
container.innerHTML = '<div style="display:flex;flex-direction:column;gap:0.75rem;margin-top:0.25rem">' + entries.map(([name, stats]) => {
|
||||
const total = stats.runs + stats.tools + stats.errors;
|
||||
const pct = maxTotal > 0 ? (total / maxTotal * 100) : 0;
|
||||
const cssClass = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
||||
return `
|
||||
<div class="fw-bar-row">
|
||||
<div class="fw-bar-label">
|
||||
<span class="fw-bar-name">${escapeHTML(name)}</span>
|
||||
<span class="fw-bar-count">${total} events</span>
|
||||
</div>
|
||||
<div class="fw-bar-track">
|
||||
<div class="fw-bar-fill ${escapeHTML(cssClass)}" style="width:${pct}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return barRow({
|
||||
name,
|
||||
count: total,
|
||||
countDisplay: total + ' events',
|
||||
max: maxTotal,
|
||||
fwClass: cssClass,
|
||||
size: 'lg',
|
||||
});
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
|
||||
@@ -763,61 +746,21 @@ function renderDashFeed() {
|
||||
function renderDashTopTools() {
|
||||
const list = document.getElementById('dash-top-tools');
|
||||
if (!list) return;
|
||||
|
||||
const topTools = Object.entries(dashboardState.toolCounts)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10);
|
||||
|
||||
if (topTools.length === 0) {
|
||||
list.innerHTML = '<li style="color:var(--text-dim);font-size:0.8rem">No tool data yet</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
const maxCount = topTools[0]?.[1] || 1;
|
||||
list.innerHTML = topTools.map(([name, count]) => {
|
||||
const pct = (count / maxCount * 100).toFixed(1);
|
||||
return `
|
||||
<li>
|
||||
<div class="stat-list-header">
|
||||
<span class="stat-list-name">${escapeHTML(name)}</span>
|
||||
<span class="stat-list-count">${count}</span>
|
||||
</div>
|
||||
<div class="stat-list-bar-track">
|
||||
<div class="stat-list-bar-fill" style="width:${pct}%"></div>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
}).join('');
|
||||
.slice(0, 10)
|
||||
.map(([name, count]) => ({ name, count }));
|
||||
list.innerHTML = barRankList(topTools, { emptyText: 'No tool data yet' });
|
||||
}
|
||||
|
||||
function renderDashTopModels() {
|
||||
const list = document.getElementById('dash-top-models');
|
||||
if (!list) return;
|
||||
|
||||
const topModels = Object.entries(dashboardState.modelCounts)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10);
|
||||
|
||||
if (topModels.length === 0) {
|
||||
list.innerHTML = '<li style="color:var(--text-dim);font-size:0.8rem">No model data yet</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
const maxCount = topModels[0]?.[1] || 1;
|
||||
list.innerHTML = topModels.map(([name, count]) => {
|
||||
const pct = (count / maxCount * 100).toFixed(1);
|
||||
return `
|
||||
<li>
|
||||
<div class="stat-list-header">
|
||||
<span class="stat-list-name">${escapeHTML(name)}</span>
|
||||
<span class="stat-list-count">${count}</span>
|
||||
</div>
|
||||
<div class="stat-list-bar-track">
|
||||
<div class="stat-list-bar-fill model" style="width:${pct}%"></div>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
}).join('');
|
||||
.slice(0, 10)
|
||||
.map(([name, count]) => ({ name, count, modifier: 'model' }));
|
||||
list.innerHTML = barRankList(topModels, { emptyText: 'No model data yet' });
|
||||
}
|
||||
|
||||
// ── Exports ──────────────────────────────────────────────
|
||||
@@ -863,38 +806,21 @@ export async function renderDashboard(routeToken) {
|
||||
<div class="summary-card-sub" id="dash-errors-sub"> </div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metrics-strip">
|
||||
<div class="metric-pill">
|
||||
<span class="metric-pill-label">Tokens today</span>
|
||||
<span class="metric-pill-value" id="dash-tokens-today">-</span>
|
||||
</div>
|
||||
<div class="metric-pill">
|
||||
<span class="metric-pill-label">Cost today</span>
|
||||
<span class="metric-pill-value" id="dash-cost-today">-</span>
|
||||
</div>
|
||||
<div class="metric-pill">
|
||||
<span class="metric-pill-label">Avg run duration</span>
|
||||
<span class="metric-pill-value" id="dash-avg-duration">-</span>
|
||||
</div>
|
||||
<div class="metric-pill">
|
||||
<span class="metric-pill-label">Error rate</span>
|
||||
<span class="metric-pill-value metric-pill-alert" id="dash-error-rate">-</span>
|
||||
</div>
|
||||
<div class="metric-pill">
|
||||
<span class="metric-pill-label">Cost / run</span>
|
||||
<span class="metric-pill-value" id="dash-cost-per-run">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom:1.25rem">${metricStrip([
|
||||
{ label: 'Tokens today', valueId: 'dash-tokens-today' },
|
||||
{ label: 'Cost today', valueId: 'dash-cost-today' },
|
||||
{ label: 'Avg run duration', valueId: 'dash-avg-duration' },
|
||||
{ label: 'Error rate', valueId: 'dash-error-rate' },
|
||||
{ label: 'Cost / run', valueId: 'dash-cost-per-run' },
|
||||
])}</div>
|
||||
<div class="section-title" style="margin-bottom:0.75rem">Infrastructure</div>
|
||||
<div class="vm-strip" id="dash-vm-strip"></div>
|
||||
<div class="charts-row">
|
||||
<div class="chart-panel">
|
||||
<div class="chart-header">
|
||||
<div class="chart-title-group">
|
||||
<span class="chart-title">Event Rate</span>
|
||||
<span class="chart-subtitle">Runs, tool spans, and errors over time</span>
|
||||
</div>
|
||||
<div class="chart-header-controls">
|
||||
${chartHeader({
|
||||
title: 'Event Rate',
|
||||
subtitle: 'Runs, tool spans, and errors over time',
|
||||
controls: `
|
||||
<div class="chart-legend">
|
||||
<span class="chart-legend-item"><span class="chart-legend-dot total"></span>total</span>
|
||||
<span class="chart-legend-item"><span class="chart-legend-dot" style="background:#34d399"></span>runs</span>
|
||||
@@ -910,21 +836,21 @@ export async function renderDashboard(routeToken) {
|
||||
<button class="window-btn" data-w="6h">6h</button>
|
||||
<button class="window-btn" data-w="24h">24h</button>
|
||||
<button class="window-btn" data-w="7d">7d</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-insights" id="dash-chart-insights"></div>
|
||||
</div>`,
|
||||
})}
|
||||
<div id="dash-chart-insights" style="margin-bottom:0.9rem"></div>
|
||||
<div class="chart-container" id="dash-chart"></div>
|
||||
<div class="chart-hover-panel" id="dash-chart-hover"></div>
|
||||
</div>
|
||||
<div class="chart-panel right-panel">
|
||||
<div class="chart-header">
|
||||
<div class="right-panel-tabs" id="dash-right-tabs">
|
||||
<button class="right-panel-tab ${dashboardState.rightPanelMode === 'framework' ? 'active' : ''}" data-panel="framework">Framework</button>
|
||||
<button class="right-panel-tab ${dashboardState.rightPanelMode === 'tokens' ? 'active' : ''}" data-panel="tokens">Tokens</button>
|
||||
<button class="right-panel-tab ${dashboardState.rightPanelMode === 'latency' ? 'active' : ''}" data-panel="latency">Latency</button>
|
||||
</div>
|
||||
</div>
|
||||
${chartHeader({
|
||||
controls: `
|
||||
<div class="right-panel-tabs" id="dash-right-tabs">
|
||||
<button class="right-panel-tab ${dashboardState.rightPanelMode === 'framework' ? 'active' : ''}" data-panel="framework">Framework</button>
|
||||
<button class="right-panel-tab ${dashboardState.rightPanelMode === 'tokens' ? 'active' : ''}" data-panel="tokens">Tokens</button>
|
||||
<button class="right-panel-tab ${dashboardState.rightPanelMode === 'latency' ? 'active' : ''}" data-panel="latency">Latency</button>
|
||||
</div>`,
|
||||
})}
|
||||
<div class="right-panel-body" id="dash-right-panel">
|
||||
<p class="empty-state" style="padding:1rem">Loading...</p>
|
||||
</div>
|
||||
@@ -932,28 +858,20 @@ export async function renderDashboard(routeToken) {
|
||||
</div>
|
||||
<div class="bottom-panels">
|
||||
<div class="feed-panel">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">Recent Activity</span>
|
||||
</div>
|
||||
${chartHeader({ title: 'Recent Activity' })}
|
||||
<div class="timeline" id="dash-feed">
|
||||
<p class="empty-state" style="padding:1rem">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tools-panel">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">Top Usage</span>
|
||||
</div>
|
||||
${chartHeader({ title: 'Top Usage' })}
|
||||
<div class="usage-rank-group">
|
||||
<div class="usage-rank-header">Tools</div>
|
||||
<ul class="stat-list" id="dash-top-tools">
|
||||
<li style="color:var(--text-dim);font-size:0.8rem">Loading...</li>
|
||||
</ul>
|
||||
<div id="dash-top-tools"><p class="empty-state" style="padding:0.5rem 0;font-size:0.8rem">Loading...</p></div>
|
||||
</div>
|
||||
<div class="usage-rank-group">
|
||||
<div class="usage-rank-header">Models</div>
|
||||
<ul class="stat-list" id="dash-top-models">
|
||||
<li style="color:var(--text-dim);font-size:0.8rem">Loading...</li>
|
||||
</ul>
|
||||
<div id="dash-top-models"><p class="empty-state" style="padding:0.5rem 0;font-size:0.8rem">Loading...</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { app, isRouteCurrent } from '../router.js';
|
||||
import { api } from '../api.js';
|
||||
import { escapeHTML, formatTokenCount, formatCost } from '../utils.js';
|
||||
import { barTrack, barRankList, metricStrip, chartHeader } from '../components.js';
|
||||
|
||||
/* global uPlot */
|
||||
|
||||
@@ -104,7 +105,6 @@ function renderFrameworkBreakdown(byFw) {
|
||||
|
||||
el.innerHTML = entries.map(([name, stats]) => {
|
||||
const total = stats.runs + stats.tools + stats.errors;
|
||||
const pct = (total / maxTotal * 100).toFixed(1);
|
||||
const cssClass = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
||||
const active = (stats.active_sessions || 0) > 0;
|
||||
return `
|
||||
@@ -119,9 +119,7 @@ function renderFrameworkBreakdown(byFw) {
|
||||
<span class="usage-fw-stat"><span class="usage-fw-stat-label">tools</span>${stats.tools || 0}</span>
|
||||
${(stats.errors || 0) > 0 ? `<span class="usage-fw-stat error"><span class="usage-fw-stat-label">err</span>${stats.errors}</span>` : ''}
|
||||
</div>
|
||||
<div class="usage-fw-bar-track">
|
||||
<div class="usage-fw-bar-fill ${escapeHTML(cssClass)}" style="width:${pct}%"></div>
|
||||
</div>
|
||||
${barTrack({ value: total, max: maxTotal, fwClass: cssClass, size: 'sm' })}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
@@ -194,36 +192,22 @@ export async function renderUsage(routeToken) {
|
||||
|
||||
<div class="usage-section-row">
|
||||
<div class="usage-panel usage-chart-panel">
|
||||
<div class="usage-chart-header">
|
||||
<span class="section-title" style="margin:0">7-Day Trend</span>
|
||||
<div class="usage-chart-tabs" id="usage-chart-tabs">
|
||||
<button class="usage-chart-tab active" data-mode="activity">Activity</button>
|
||||
<button class="usage-chart-tab" data-mode="tokens">Tokens</button>
|
||||
<button class="usage-chart-tab" data-mode="cost">Cost</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usage-chart-totals">
|
||||
<span class="usage-chart-total-pill">
|
||||
<span class="usage-chart-total-label">runs</span>
|
||||
<strong>${t.runs}</strong>
|
||||
</span>
|
||||
<span class="usage-chart-total-pill">
|
||||
<span class="usage-chart-total-label">tools</span>
|
||||
<strong>${t.tools}</strong>
|
||||
</span>
|
||||
<span class="usage-chart-total-pill">
|
||||
<span class="usage-chart-total-label">errors</span>
|
||||
<strong class="${t.errors > 0 ? 'usage-total-errors' : ''}">${t.errors}</strong>
|
||||
</span>
|
||||
<span class="usage-chart-total-pill">
|
||||
<span class="usage-chart-total-label">tokens</span>
|
||||
<strong>${formatTokenCount(t.tokens)}</strong>
|
||||
</span>
|
||||
<span class="usage-chart-total-pill">
|
||||
<span class="usage-chart-total-label">cost</span>
|
||||
<strong>${formatCost(t.cost)}</strong>
|
||||
</span>
|
||||
</div>
|
||||
${chartHeader({
|
||||
title: '7-Day Trend',
|
||||
controls: `
|
||||
<div class="usage-chart-tabs" id="usage-chart-tabs">
|
||||
<button class="usage-chart-tab active" data-mode="activity">Activity</button>
|
||||
<button class="usage-chart-tab" data-mode="tokens">Tokens</button>
|
||||
<button class="usage-chart-tab" data-mode="cost">Cost</button>
|
||||
</div>`,
|
||||
})}
|
||||
${metricStrip([
|
||||
{ label: 'runs', value: String(t.runs), variant: 'total' },
|
||||
{ label: 'tools', value: String(t.tools), variant: 'total' },
|
||||
{ label: 'errors', value: String(t.errors), variant: 'total', alert: t.errors > 0 },
|
||||
{ label: 'tokens', value: formatTokenCount(t.tokens), variant: 'total' },
|
||||
{ label: 'cost', value: formatCost(t.cost), variant: 'total' },
|
||||
])}
|
||||
<div id="usage-chart"></div>
|
||||
</div>
|
||||
<div class="usage-panel usage-fw-panel">
|
||||
@@ -238,40 +222,20 @@ export async function renderUsage(routeToken) {
|
||||
<div class="usage-section-row">
|
||||
<div class="usage-panel">
|
||||
<div class="section-title">Top Models <span class="count">${models.length}</span></div>
|
||||
${models.length === 0 ? '<p class="empty-state" style="padding:1rem">No model data</p>' : `
|
||||
<ul class="stat-list">
|
||||
${models.map(m => {
|
||||
const pct = (m.count / maxModel * 100).toFixed(1);
|
||||
return `<li>
|
||||
<div class="stat-list-header">
|
||||
<span class="stat-list-name">${escapeHTML(m.name)}</span>
|
||||
<span class="stat-list-count">${m.count}</span>
|
||||
</div>
|
||||
<div class="stat-list-bar-track">
|
||||
<div class="stat-list-bar-fill model" style="width:${pct}%"></div>
|
||||
</div>
|
||||
</li>`;
|
||||
}).join('')}
|
||||
</ul>`}
|
||||
${barRankList(models, {
|
||||
mapItem: m => ({ name: m.name, count: m.count, modifier: 'model' }),
|
||||
maxOverride: maxModel,
|
||||
emptyText: 'No model data',
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="usage-panel">
|
||||
<div class="section-title">Top Tools <span class="count">${tools.length}</span></div>
|
||||
${tools.length === 0 ? '<p class="empty-state" style="padding:1rem">No tool data</p>' : `
|
||||
<ul class="stat-list">
|
||||
${tools.map(t => {
|
||||
const pct = (t.count / maxTool * 100).toFixed(1);
|
||||
return `<li>
|
||||
<div class="stat-list-header">
|
||||
<span class="stat-list-name">${escapeHTML(t.name)}</span>
|
||||
<span class="stat-list-count">${t.count}</span>
|
||||
</div>
|
||||
<div class="stat-list-bar-track">
|
||||
<div class="stat-list-bar-fill" style="width:${pct}%"></div>
|
||||
</div>
|
||||
</li>`;
|
||||
}).join('')}
|
||||
</ul>`}
|
||||
${barRankList(tools, {
|
||||
mapItem: x => ({ name: x.name, count: x.count }),
|
||||
maxOverride: maxTool,
|
||||
emptyText: 'No tool data',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user