Add object search endpoint, hide internal metadata keys, fix toast/template bugs

- Implement missing /ui/buckets/{bucket}/objects/search route used by the
  Objects tab filter; previously returned 404 and showed 'Search failed'.
- Filter __checksum_*__ and __size__ sentinels from the object metadata
  panel so users no longer see internal keys in the UI.
- Include a body field in bucket-delete flash message so the toast shows
  distinct title and body.
- Replace Tera boolean 'or' operator with if/else fallback in
  replication_wizard.html, sites.html, iam.html.
This commit is contained in:
2026-04-25 00:58:49 +08:00
parent 37541ffba1
commit 7e32ac2a46
9 changed files with 221 additions and 17 deletions

View File

@@ -336,6 +336,72 @@
}
};
const renderObjectsLimit = (totalObjects, maxObjects) => {
if (maxObjects && maxObjects > 0) {
const pct = Math.min(100, Math.floor(totalObjects / maxObjects * 100));
const cls = pct >= 90 ? 'bg-danger' : pct >= 75 ? 'bg-warning' : 'bg-success';
return '<div class="progress mt-2" style="height: 4px;">' +
'<div class="progress-bar ' + cls + '" style="width: ' + pct + '%"></div>' +
'</div>' +
'<div class="small text-muted mt-1">' + pct + '% of ' + maxObjects.toLocaleString() + ' limit</div>';
}
return '<div class="small text-muted mt-2">No limit</div>';
};
const renderBytesLimit = (totalBytes, maxBytes) => {
if (maxBytes && maxBytes > 0) {
const pct = Math.min(100, Math.floor(totalBytes / maxBytes * 100));
const cls = pct >= 90 ? 'bg-danger' : pct >= 75 ? 'bg-warning' : 'bg-success';
return '<div class="progress mt-2" style="height: 4px;">' +
'<div class="progress-bar ' + cls + '" style="width: ' + pct + '%"></div>' +
'</div>' +
'<div class="small text-muted mt-1">' + pct + '% of ' + formatBytes(maxBytes) + ' limit</div>';
}
return '<div class="small text-muted mt-2">No limit</div>';
};
const redrawUsageLimits = () => {
const objectsCard = document.querySelector('[data-usage-objects]');
const objectsLimit = document.querySelector('[data-usage-objects-limit]');
if (objectsCard && objectsLimit) {
const totalObjects = parseInt(objectsCard.dataset.totalObjects || '0', 10);
const maxObjectsRaw = objectsCard.dataset.maxObjects;
const maxObjects = maxObjectsRaw ? parseInt(maxObjectsRaw, 10) : 0;
objectsLimit.innerHTML = renderObjectsLimit(totalObjects, maxObjects);
}
const bytesCard = document.querySelector('[data-usage-bytes]');
const bytesLimit = document.querySelector('[data-usage-bytes-limit]');
if (bytesCard && bytesLimit) {
const totalBytes = parseInt(bytesCard.dataset.totalBytes || '0', 10);
const maxBytesRaw = bytesCard.dataset.maxBytes;
const maxBytes = maxBytesRaw ? parseInt(maxBytesRaw, 10) : 0;
bytesLimit.innerHTML = renderBytesLimit(totalBytes, maxBytes);
}
};
const refreshBucketUsage = async () => {
try {
const bucketName = objectsContainer?.dataset.bucket;
if (!bucketName) return;
const url = `/ui/buckets/${encodeURIComponent(bucketName)}/stats`;
const response = await fetch(url, { headers: { 'Accept': 'application/json' } });
if (!response.ok) return;
const data = await response.json();
const objectsCard = document.querySelector('[data-usage-objects]');
const objectsValue = document.querySelector('[data-usage-objects-value]');
if (objectsCard) objectsCard.dataset.totalObjects = String(data.total_objects);
if (objectsValue) objectsValue.textContent = data.total_objects.toLocaleString();
const bytesCard = document.querySelector('[data-usage-bytes]');
const bytesValue = document.querySelector('[data-usage-bytes-value]');
if (bytesCard) bytesCard.dataset.totalBytes = String(data.total_bytes);
if (bytesValue) bytesValue.textContent = formatBytes(data.total_bytes);
redrawUsageLimits();
} catch (e) { }
};
let topSpacer = null;
let bottomSpacer = null;
@@ -660,6 +726,10 @@
break;
case 'count':
totalObjectCount = msg.total_count || 0;
if (!currentPrefix) {
bucketTotalObjects = totalObjectCount;
updateObjectCountBadge();
}
if (objectsLoadingRow) {
const loadingText = objectsLoadingRow.querySelector('p');
if (loadingText) loadingText.textContent = `Loading 0 of ${totalObjectCount.toLocaleString()} objects...`;
@@ -770,7 +840,7 @@
}
totalObjectCount = data.total_count || 0;
if (!append && !currentPrefix && !useDelimiterMode) bucketTotalObjects = totalObjectCount;
if (!append && !currentPrefix) bucketTotalObjects = totalObjectCount;
nextContinuationToken = data.next_continuation_token;
if (!append && objectsLoadingRow) {
@@ -1491,6 +1561,7 @@
previewPanel.classList.add('d-none');
activeRow = null;
loadObjects(false);
refreshBucketUsage();
} catch (error) {
bulkDeleteModal?.hide();
showMessage({ title: 'Delete failed', body: (error && error.message) || 'Unable to delete selected objects', variant: 'danger' });
@@ -1966,6 +2037,7 @@
previewPanel.classList.add('d-none');
activeRow = null;
loadObjects(false);
refreshBucketUsage();
} catch (err) {
if (deleteModal) deleteModal.hide();
showMessage({ title: 'Delete failed', body: err.message || 'Unable to delete object', variant: 'danger' });
@@ -3071,6 +3143,7 @@
} else if (errorCount > 0) {
showMessage({ title: 'Upload failed', body: `${errorCount} file(s) failed to upload.`, variant: 'danger' });
}
if (successCount > 0) refreshBucketUsage();
};
const performBulkUpload = async (files) => {
@@ -4542,6 +4615,16 @@
var maxObjInput = document.getElementById('max_objects');
if (maxMbInput) maxMbInput.value = maxBytes ? Math.floor(maxBytes / 1048576) : '';
if (maxObjInput) maxObjInput.value = maxObjects || '';
var objectsCard = document.querySelector('[data-usage-objects]');
if (objectsCard) {
objectsCard.dataset.maxObjects = maxObjects && maxObjects > 0 ? String(maxObjects) : '';
}
var bytesCard = document.querySelector('[data-usage-bytes]');
if (bytesCard) {
bytesCard.dataset.maxBytes = maxBytes && maxBytes > 0 ? String(maxBytes) : '';
}
redrawUsageLimits();
}
function updatePolicyCard(hasPolicy, preset) {
@@ -4815,7 +4898,7 @@
e.preventDefault();
window.UICore.submitFormAjax(deleteBucketForm, {
onSuccess: function () {
sessionStorage.setItem('flashMessage', JSON.stringify({ title: 'Bucket deleted', variant: 'success' }));
sessionStorage.setItem('flashMessage', JSON.stringify({ title: 'Success', body: 'Bucket deleted', variant: 'success' }));
window.location.href = window.BucketDetailConfig?.endpoints?.bucketsOverview || '/ui/buckets';
}
});