ui: add mobile secret cards
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user