ui: add mobile secret cards

This commit is contained in:
a.tolmachev
2026-05-03 19:30:36 +00:00
parent 79d878bd92
commit 51c70cc1d0
3 changed files with 117 additions and 1 deletions
+2 -1
View File
@@ -49,8 +49,9 @@ Verification:
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
- pending:
- mobile card layouts for `Secrets` and `Usage`
- mobile card layout for `Usage`
- remaining `Agents` and `Settings` commercial UX polish
## Planned
+8
View File
@@ -39,6 +39,11 @@
color: var(--text-primary);
font-weight: 600;
}
.secrets-card-list { display: none; }
@media (max-width: 720px) {
#secrets-table-wrap { display: none; }
.secrets-card-list { display: grid; }
}
</style>
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
</head>
@@ -138,6 +143,7 @@
</div>
</div>
<div class="section-card-body" style="padding-top: 0;">
<div id="secrets-table-wrap">
<table class="data-table">
<thead>
<tr>
@@ -152,6 +158,8 @@
</thead>
<tbody id="secrets-tbody"></tbody>
</table>
</div>
<div class="resource-list secrets-card-list" id="secrets-card-list"></div>
</div>
</div>
+107
View File
@@ -17,6 +17,7 @@ function initSecretsPage() {
var modal = document.getElementById('secret-modal');
var tbody = document.getElementById('secrets-tbody');
var cardList = document.getElementById('secrets-card-list');
var profilesList = document.getElementById('auth-profiles-list');
var summary = document.getElementById('secrets-summary');
var profilesSummary = document.getElementById('secret-profiles-summary');
@@ -248,6 +249,9 @@ function initSecretsPage() {
});
window.CrankDom.clear(tbody);
if (cardList) {
window.CrankDom.clear(cardList);
}
if (state.loading && state.secrets.length === 0) {
var loadingRow = document.createElement('tr');
@@ -257,6 +261,7 @@ function initSecretsPage() {
loadingCell.textContent = tKey('secrets.loading');
loadingRow.appendChild(loadingCell);
tbody.appendChild(loadingRow);
renderSecretCards([], tKey('secrets.loading'));
return;
}
@@ -268,6 +273,7 @@ function initSecretsPage() {
errorCell.textContent = state.error;
errorRow.appendChild(errorCell);
tbody.appendChild(errorRow);
renderSecretCards([], state.error);
return;
}
@@ -279,6 +285,7 @@ function initSecretsPage() {
emptyCell.textContent = state.secrets.length ? tKey('secrets.empty.search') : tKey('secrets.empty.none');
emptyRow.appendChild(emptyCell);
tbody.appendChild(emptyRow);
renderSecretCards(rows);
return;
}
@@ -340,6 +347,106 @@ function initSecretsPage() {
tbody.appendChild(tr);
});
renderSecretCards(rows);
}
function renderSecretCards(rows, statusText) {
if (!cardList) {
return;
}
window.CrankDom.clear(cardList);
if (statusText && !rows.length) {
var messageCard = document.createElement('div');
messageCard.className = 'resource-card';
var message = document.createElement('div');
message.className = 'resource-meta-value';
message.textContent = statusText;
message.style.color = state.error ? 'var(--danger,#f85149)' : 'var(--text-muted)';
messageCard.appendChild(message);
cardList.appendChild(messageCard);
return;
}
rows.forEach(function(secret) {
var references = state.derived.usageBySecret[secret.id] || [];
var card = document.createElement('div');
card.className = 'resource-card';
var header = document.createElement('div');
header.className = 'resource-card-header';
var headerBody = document.createElement('div');
var title = document.createElement('div');
title.className = 'resource-card-title';
title.textContent = secret.name;
var subtitle = document.createElement('div');
subtitle.className = 'resource-card-subtitle';
subtitle.textContent = secretKindLabel(secret.kind);
headerBody.appendChild(title);
headerBody.appendChild(subtitle);
var actions = document.createElement('div');
actions.className = 'resource-card-actions';
var badge = document.createElement('span');
badge.className = 'badge ' + (secret.status === 'disabled' ? 'badge-revoked' : 'badge-active');
badge.textContent = tKey('secrets.status.' + secret.status);
actions.appendChild(badge);
header.appendChild(headerBody);
header.appendChild(actions);
card.appendChild(header);
var metaGrid = document.createElement('div');
metaGrid.className = 'resource-meta-grid';
metaGrid.appendChild(buildSecretMetaItem('secrets.th.version', 'v' + secret.current_version));
metaGrid.appendChild(buildSecretMetaItem(
'secrets.th.last_used',
secret.last_used_at ? formatDate(secret.last_used_at) : tKey('secrets.last_used.never')
));
metaGrid.appendChild(buildSecretMetaItem(
'secrets.th.used_by',
window.tPlural('secrets.used_by_count', references.length, { count: references.length })
));
card.appendChild(metaGrid);
var actionRow = document.createElement('div');
actionRow.className = 'resource-card-actions';
var rotateButton = document.createElement('button');
rotateButton.className = 'btn-secondary';
rotateButton.type = 'button';
rotateButton.textContent = tKey('secrets.action.rotate');
rotateButton.addEventListener('click', function() {
openModal('rotate', secret);
});
actionRow.appendChild(rotateButton);
var deleteButton = document.createElement('button');
deleteButton.className = 'btn-secondary';
deleteButton.type = 'button';
deleteButton.textContent = tKey('secrets.action.delete');
deleteButton.addEventListener('click', async function() {
await deleteSecret(secret);
});
actionRow.appendChild(deleteButton);
card.appendChild(actionRow);
cardList.appendChild(card);
});
}
function buildSecretMetaItem(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 renderProfiles() {