ui: add mobile secret cards
This commit is contained in:
@@ -49,8 +49,9 @@ Verification:
|
|||||||
Progress:
|
Progress:
|
||||||
- done:
|
- done:
|
||||||
- `API Keys` page now has a mobile card layout instead of relying only on desktop table presentation
|
- `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:
|
- pending:
|
||||||
- mobile card layouts for `Secrets` and `Usage`
|
- mobile card layout for `Usage`
|
||||||
- remaining `Agents` and `Settings` commercial UX polish
|
- remaining `Agents` and `Settings` commercial UX polish
|
||||||
|
|
||||||
## Planned
|
## Planned
|
||||||
|
|||||||
@@ -39,6 +39,11 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
.secrets-card-list { display: none; }
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
#secrets-table-wrap { display: none; }
|
||||||
|
.secrets-card-list { display: grid; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -138,6 +143,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-card-body" style="padding-top: 0;">
|
<div class="section-card-body" style="padding-top: 0;">
|
||||||
|
<div id="secrets-table-wrap">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -152,6 +158,8 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="secrets-tbody"></tbody>
|
<tbody id="secrets-tbody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="resource-list secrets-card-list" id="secrets-card-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ function initSecretsPage() {
|
|||||||
|
|
||||||
var modal = document.getElementById('secret-modal');
|
var modal = document.getElementById('secret-modal');
|
||||||
var tbody = document.getElementById('secrets-tbody');
|
var tbody = document.getElementById('secrets-tbody');
|
||||||
|
var cardList = document.getElementById('secrets-card-list');
|
||||||
var profilesList = document.getElementById('auth-profiles-list');
|
var profilesList = document.getElementById('auth-profiles-list');
|
||||||
var summary = document.getElementById('secrets-summary');
|
var summary = document.getElementById('secrets-summary');
|
||||||
var profilesSummary = document.getElementById('secret-profiles-summary');
|
var profilesSummary = document.getElementById('secret-profiles-summary');
|
||||||
@@ -248,6 +249,9 @@ function initSecretsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.CrankDom.clear(tbody);
|
window.CrankDom.clear(tbody);
|
||||||
|
if (cardList) {
|
||||||
|
window.CrankDom.clear(cardList);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.loading && state.secrets.length === 0) {
|
if (state.loading && state.secrets.length === 0) {
|
||||||
var loadingRow = document.createElement('tr');
|
var loadingRow = document.createElement('tr');
|
||||||
@@ -257,6 +261,7 @@ function initSecretsPage() {
|
|||||||
loadingCell.textContent = tKey('secrets.loading');
|
loadingCell.textContent = tKey('secrets.loading');
|
||||||
loadingRow.appendChild(loadingCell);
|
loadingRow.appendChild(loadingCell);
|
||||||
tbody.appendChild(loadingRow);
|
tbody.appendChild(loadingRow);
|
||||||
|
renderSecretCards([], tKey('secrets.loading'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +273,7 @@ function initSecretsPage() {
|
|||||||
errorCell.textContent = state.error;
|
errorCell.textContent = state.error;
|
||||||
errorRow.appendChild(errorCell);
|
errorRow.appendChild(errorCell);
|
||||||
tbody.appendChild(errorRow);
|
tbody.appendChild(errorRow);
|
||||||
|
renderSecretCards([], state.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,6 +285,7 @@ function initSecretsPage() {
|
|||||||
emptyCell.textContent = state.secrets.length ? tKey('secrets.empty.search') : tKey('secrets.empty.none');
|
emptyCell.textContent = state.secrets.length ? tKey('secrets.empty.search') : tKey('secrets.empty.none');
|
||||||
emptyRow.appendChild(emptyCell);
|
emptyRow.appendChild(emptyCell);
|
||||||
tbody.appendChild(emptyRow);
|
tbody.appendChild(emptyRow);
|
||||||
|
renderSecretCards(rows);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,6 +347,106 @@ function initSecretsPage() {
|
|||||||
|
|
||||||
tbody.appendChild(tr);
|
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() {
|
function renderProfiles() {
|
||||||
|
|||||||
Reference in New Issue
Block a user