ui: add mobile api key cards

This commit is contained in:
a.tolmachev
2026-05-03 19:27:53 +00:00
parent 34d345158f
commit 79d878bd92
3 changed files with 134 additions and 0 deletions
+7
View File
@@ -46,6 +46,13 @@ Verification:
- manual mobile checks at 375px width;
- EN/RU copy audit.
Progress:
- done:
- `API Keys` page now has a mobile card layout instead of relying only on desktop table presentation
- pending:
- mobile card layouts for `Secrets` and `Usage`
- remaining `Agents` and `Settings` commercial UX polish
## Planned
### `feat/open-core-repo-boundary`
+6
View File
@@ -22,6 +22,11 @@
}
.scope-checkbox-name { font-size: 13px; font-weight: 500; color: var(--text-primary); }
.scope-checkbox-desc { font-size: 11.5px; color: var(--text-muted); }
.keys-card-list { display: none; }
@media (max-width: 720px) {
#keys-table-wrap { display: none; }
.keys-card-list { display: grid; }
}
</style>
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
</head>
@@ -195,6 +200,7 @@
</tbody>
</table>
</div>
<div class="resource-list keys-card-list" id="keys-card-list"></div>
</div>
<div class="section-card">
+121
View File
@@ -291,12 +291,16 @@ function renderScopes() {
function renderTable(errorMessage) {
var q = search.toLowerCase();
var tbody = document.getElementById('keys-tbody');
var cardList = document.getElementById('keys-card-list');
var rows = KEYS.filter(function(key) {
return !q || key.name.toLowerCase().includes(q) || key.prefix.toLowerCase().includes(q);
});
var subtitle = document.getElementById('keys-summary-subtitle');
tbody.innerHTML = '';
if (cardList) {
cardList.innerHTML = '';
}
if (subtitle) {
var active = KEYS.filter(function(key) { return key.status === 'active'; }).length;
@@ -314,6 +318,7 @@ function renderTable(errorMessage) {
errorCell.textContent = errorMessage;
errorRow.appendChild(errorCell);
tbody.appendChild(errorRow);
renderKeyCards([], errorMessage);
return;
}
@@ -327,6 +332,7 @@ function renderTable(errorMessage) {
: tKey('apikeys.agent.empty_hint');
empty.appendChild(td);
tbody.appendChild(empty);
renderKeyCards(rows);
return;
}
@@ -370,6 +376,121 @@ function renderTable(errorMessage) {
tbody.appendChild(node);
});
renderKeyCards(rows);
}
function renderKeyCards(rows, errorMessage) {
var cardList = document.getElementById('keys-card-list');
if (!cardList) return;
cardList.innerHTML = '';
if (errorMessage) {
cardList.appendChild(buildKeyCardMessage(errorMessage, true));
return;
}
if (!rows.length) {
cardList.appendChild(
buildKeyCardMessage(
currentAgentId
? (KEYS.length ? tKey('apikeys.empty.search') : tKey('apikeys.empty.none'))
: tKey('apikeys.agent.empty_hint'),
false
)
);
return;
}
rows.forEach(function(key) {
var card = document.createElement('div');
card.className = 'resource-card';
var header = document.createElement('div');
header.className = 'resource-card-header';
var headerMain = document.createElement('div');
var title = document.createElement('div');
title.className = 'resource-card-title';
title.textContent = key.name;
var subtitle = document.createElement('div');
subtitle.className = 'resource-card-subtitle';
subtitle.textContent = key.prefix;
headerMain.appendChild(title);
headerMain.appendChild(subtitle);
var actions = document.createElement('div');
actions.className = 'resource-card-actions';
var statusBadge = document.createElement('span');
statusBadge.className = key.status === 'active' ? 'badge badge-active' : 'badge badge-revoked';
statusBadge.textContent = key.status === 'active'
? tKey('apikeys.status.active')
: tKey('apikeys.status.revoked');
actions.appendChild(statusBadge);
header.appendChild(headerMain);
header.appendChild(actions);
card.appendChild(header);
var metaGrid = document.createElement('div');
metaGrid.className = 'resource-meta-grid';
metaGrid.appendChild(buildMetaItem('apikeys.th.scopes', key.scopes.join(', ') || '—'));
metaGrid.appendChild(buildMetaItem('apikeys.th.created', key.created));
metaGrid.appendChild(buildMetaItem('apikeys.th.last', key.lastUsed));
card.appendChild(metaGrid);
var actionRow = document.createElement('div');
actionRow.className = 'resource-card-actions';
if (key.status === 'active') {
actionRow.appendChild(buildCardAction('apikeys.action.copy_prefix', function() {
copyPrefix(key.prefix);
}));
actionRow.appendChild(buildCardAction('apikeys.action.revoke', function() {
revokeKey(key.id);
}, true));
} else {
actionRow.appendChild(buildCardAction('apikeys.action.delete', function() {
deleteKey(key.id);
}, true));
}
card.appendChild(actionRow);
cardList.appendChild(card);
});
}
function buildKeyCardMessage(text, isError) {
var card = document.createElement('div');
card.className = 'resource-card';
var value = document.createElement('div');
value.className = 'resource-meta-value';
value.textContent = text;
value.style.color = isError ? 'var(--danger,#f85149)' : 'var(--text-muted)';
card.appendChild(value);
return card;
}
function buildMetaItem(labelKey, valueText) {
var item = document.createElement('div');
item.className = 'resource-meta-item';
var label = document.createElement('div');
label.className = 'resource-meta-label';
label.textContent = tKey(labelKey);
var value = document.createElement('div');
value.className = 'resource-meta-value';
value.textContent = valueText;
item.appendChild(label);
item.appendChild(value);
return item;
}
function buildCardAction(labelKey, onClick, isDanger) {
var button = document.createElement('button');
button.type = 'button';
button.className = isDanger ? 'btn-secondary' : 'btn-secondary';
button.textContent = tKey(labelKey);
button.addEventListener('click', onClick);
return button;
}
var modal = document.getElementById('modal-create');