ui: add mobile usage cards

This commit is contained in:
a.tolmachev
2026-05-03 19:31:48 +00:00
parent 51c70cc1d0
commit 9b3444f2f0
3 changed files with 85 additions and 1 deletions
+1 -1
View File
@@ -50,8 +50,8 @@ Progress:
- done:
- `API Keys` page now has a mobile card layout instead of relying only on desktop table presentation
- `Secrets` page now has a mobile card layout instead of relying only on desktop table presentation
- `Usage` page now has a mobile card layout instead of relying only on desktop table presentation
- pending:
- mobile card layout for `Usage`
- remaining `Agents` and `Settings` commercial UX polish
## Planned
+10
View File
@@ -10,6 +10,13 @@
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/pages.css">
<link rel="stylesheet" href="css/usage.css">
<style>
.usage-card-list { display: none; }
@media (max-width: 720px) {
#usage-table-wrap { display: none; }
.usage-card-list { display: grid; }
}
</style>
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
</head>
<body>
@@ -162,6 +169,7 @@
<div class="section-card-subtitle">Breakdown for last 7 days</div>
</div>
</div>
<div id="usage-table-wrap">
<table class="data-table">
<thead>
<tr>
@@ -177,6 +185,8 @@
<tbody id="usage-tbody">
</tbody>
</table>
</div>
<div class="resource-list usage-card-list" id="usage-card-list"></div>
</div>
</div>
+74
View File
@@ -17,6 +17,7 @@ document.addEventListener('DOMContentLoaded', function () {
var exportBtn = document.querySelector('.page-header-actions .btn-secondary');
var chartWrap = document.getElementById('chart-bars');
var tableBody = document.getElementById('usage-tbody');
var cardList = document.getElementById('usage-card-list');
var chartTemplate = document.getElementById('tmpl-chart-bar');
var rowTemplate = document.getElementById('tmpl-usage-row');
var subtitle = document.querySelector('.section-card-subtitle');
@@ -330,6 +331,9 @@ document.addEventListener('DOMContentLoaded', function () {
function renderTable() {
var operations = state.derived.localizedOperations;
tableBody.innerHTML = '';
if (cardList) {
cardList.innerHTML = '';
}
if (!operations.length) {
var row = document.createElement('tr');
@@ -345,6 +349,7 @@ document.addEventListener('DOMContentLoaded', function () {
);
row.appendChild(cell);
tableBody.appendChild(row);
renderOperationCards([]);
return;
}
@@ -387,6 +392,75 @@ document.addEventListener('DOMContentLoaded', function () {
tableBody.appendChild(node);
});
renderOperationCards(operations);
}
function renderOperationCards(operations) {
if (!cardList) {
return;
}
cardList.innerHTML = '';
if (!operations.length) {
cardList.appendChild(buildEmptyState(
tKey('usage.table.empty.title'),
tKey('usage.table.empty.sub'),
false
));
return;
}
operations.forEach(function(entry) {
var operation = entry.operation;
var localizedOperation = entry.localized;
var errorRate = operation.calls_total === 0
? 0
: ((operation.calls_error / operation.calls_total) * 100);
var share = state.derived.totalCalls === 0
? 0
: ((operation.calls_total / state.derived.totalCalls) * 100);
var card = element('div', 'resource-card');
var header = element('div', 'resource-card-header');
var headerMain = element('div');
headerMain.appendChild(element(
'div',
'resource-card-title',
localizedOperation.operation_display_name || operation.operation_display_name
));
headerMain.appendChild(element('div', 'resource-card-subtitle', operation.operation_name));
var actions = element('div', 'resource-card-actions');
var protocolBadge = element('span', 'badge');
protocolBadge.textContent = protocolLabel(operation.protocol);
protocolBadge.style.color = protocolColor(operation.protocol);
actions.appendChild(protocolBadge);
header.appendChild(headerMain);
header.appendChild(actions);
card.appendChild(header);
var metaGrid = element('div', 'resource-meta-grid');
metaGrid.appendChild(buildUsageMetaItem('usage.table.th.calls', formatCount(operation.calls_total)));
metaGrid.appendChild(buildUsageMetaItem('usage.table.th.errors', formatCount(operation.calls_error)));
metaGrid.appendChild(buildUsageMetaItem('usage.table.th.error_rate', errorRate.toFixed(2) + '%'));
metaGrid.appendChild(buildUsageMetaItem(
'usage.table.th.latency',
formatMs(operation.p50_ms) + ' / ' + formatMs(operation.p95_ms) + ' / ' + formatMs(operation.p99_ms)
));
metaGrid.appendChild(buildUsageMetaItem('usage.table.th.share', tfKey('usage.table.share', { value: share.toFixed(1) })));
card.appendChild(metaGrid);
cardList.appendChild(card);
});
}
function buildUsageMetaItem(labelKey, valueText) {
var item = element('div', 'resource-meta-item');
item.appendChild(element('div', 'resource-meta-label', tKey(labelKey)));
item.appendChild(element('div', 'resource-meta-value', valueText));
return item;
}
function renderUsage() {