Clean up code comments

This commit is contained in:
2025-12-31 18:00:03 +08:00
parent 05f1b00473
commit 1df8ff9d25
11 changed files with 142 additions and 347 deletions

View File

@@ -968,8 +968,7 @@
{% endif %}
</div>
</div>
<!-- Warning alert for unreachable endpoint (shown by JS if endpoint is down) -->
<div id="replication-endpoint-warning" class="alert alert-danger d-none mb-4" role="alert">
<div class="d-flex align-items-start">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="flex-shrink-0 me-2" viewBox="0 0 16 16">
@@ -1783,7 +1782,6 @@
{% block extra_scripts %}
<script>
// Auto-indent for JSON textareas
function setupJsonAutoIndent(textarea) {
if (!textarea) return;
@@ -1795,15 +1793,12 @@
const end = this.selectionEnd;
const value = this.value;
// Get the current line
const lineStart = value.lastIndexOf('\n', start - 1) + 1;
const currentLine = value.substring(lineStart, start);
// Calculate base indentation (leading whitespace of current line)
const indentMatch = currentLine.match(/^(\s*)/);
let indent = indentMatch ? indentMatch[1] : '';
// Check if the line ends with { or [ (should increase indent)
const trimmedLine = currentLine.trim();
const lastChar = trimmedLine.slice(-1);
@@ -1811,42 +1806,34 @@
let insertAfter = '';
if (lastChar === '{' || lastChar === '[') {
// Add extra indentation
newIndent = indent + ' ';
// Check if we need to add closing bracket on new line
const charAfterCursor = value.substring(start, start + 1).trim();
if ((lastChar === '{' && charAfterCursor === '}') ||
(lastChar === '[' && charAfterCursor === ']')) {
insertAfter = '\n' + indent;
}
} else if (lastChar === ',' || lastChar === ':') {
// Keep same indentation for continuation
newIndent = indent;
}
// Insert newline with proper indentation
const insertion = '\n' + newIndent + insertAfter;
const newValue = value.substring(0, start) + insertion + value.substring(end);
this.value = newValue;
// Set cursor position after the indentation
const newCursorPos = start + 1 + newIndent.length;
this.selectionStart = this.selectionEnd = newCursorPos;
// Trigger input event for any listeners
this.dispatchEvent(new Event('input', { bubbles: true }));
}
// Handle Tab key for indentation
if (e.key === 'Tab') {
e.preventDefault();
const start = this.selectionStart;
const end = this.selectionEnd;
if (e.shiftKey) {
// Outdent: remove 2 spaces from start of line
const lineStart = this.value.lastIndexOf('\n', start - 1) + 1;
const lineContent = this.value.substring(lineStart, start);
if (lineContent.startsWith(' ')) {
@@ -1855,7 +1842,6 @@
this.selectionStart = this.selectionEnd = Math.max(lineStart, start - 2);
}
} else {
// Indent: insert 2 spaces
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
this.selectionStart = this.selectionEnd = start + 2;
}
@@ -1865,7 +1851,6 @@
});
}
// Apply auto-indent to policy editor textarea
setupJsonAutoIndent(document.getElementById('policyDocument'));
const formatBytes = (bytes) => {
@@ -1970,24 +1955,21 @@
let isLoadingObjects = false;
let hasMoreObjects = false;
let currentFilterTerm = '';
let pageSize = 5000; // Load large batches for virtual scrolling
let currentPrefix = ''; // Current folder prefix for navigation
let allObjects = []; // All loaded object metadata (lightweight)
let urlTemplates = null; // URL templates from API for constructing object URLs
let pageSize = 5000;
let currentPrefix = '';
let allObjects = [];
let urlTemplates = null;
// Helper to build URL from template by replacing KEY_PLACEHOLDER with encoded key
const buildUrlFromTemplate = (template, key) => {
if (!template) return '';
return template.replace('KEY_PLACEHOLDER', encodeURIComponent(key).replace(/%2F/g, '/'));
};
// Virtual scrolling state
const ROW_HEIGHT = 53; // Height of each table row in pixels
const BUFFER_ROWS = 10; // Extra rows to render above/below viewport
let visibleItems = []; // Current items to display (filtered by folder/search)
let renderedRange = { start: 0, end: 0 }; // Currently rendered row indices
const ROW_HEIGHT = 53;
const BUFFER_ROWS = 10;
let visibleItems = [];
let renderedRange = { start: 0, end: 0 };
// Create a row element from object data (for virtual scrolling)
const createObjectRow = (obj, displayKey = null) => {
const tr = document.createElement('tr');
tr.dataset.objectRow = '';
@@ -2110,16 +2092,12 @@
}
};
// ============== VIRTUAL SCROLLING SYSTEM ==============
// Spacer elements for virtual scroll height
let topSpacer = null;
let bottomSpacer = null;
const initVirtualScrollElements = () => {
if (!objectsTableBody) return;
// Create spacer rows if they don't exist
if (!topSpacer) {
topSpacer = document.createElement('tr');
topSpacer.id = 'virtual-top-spacer';
@@ -2131,38 +2109,33 @@
bottomSpacer.innerHTML = '<td colspan="4" style="padding: 0; border: none;"></td>';
}
};
// Compute which items should be visible based on current view
const computeVisibleItems = () => {
const items = [];
const folders = new Set();
allObjects.forEach(obj => {
if (!obj.key.startsWith(currentPrefix)) return;
const remainder = obj.key.slice(currentPrefix.length);
const slashIndex = remainder.indexOf('/');
if (slashIndex === -1) {
// File in current folder - filter on the displayed filename (remainder)
if (!currentFilterTerm || remainder.toLowerCase().includes(currentFilterTerm)) {
items.push({ type: 'file', data: obj, displayKey: remainder });
}
} else {
// Folder
const folderName = remainder.slice(0, slashIndex);
const folderPath = currentPrefix + folderName + '/';
if (!folders.has(folderPath)) {
folders.add(folderPath);
// Filter on the displayed folder name only
if (!currentFilterTerm || folderName.toLowerCase().includes(currentFilterTerm)) {
items.push({ type: 'folder', path: folderPath, displayKey: folderName });
}
}
}
});
// Sort: folders first, then files
items.sort((a, b) => {
if (a.type === 'folder' && b.type === 'file') return -1;
if (a.type === 'file' && b.type === 'folder') return 1;
@@ -2173,36 +2146,30 @@
return items;
};
// Render only the visible rows based on scroll position
const renderVirtualRows = () => {
if (!objectsTableBody || !scrollContainer) return;
const containerHeight = scrollContainer.clientHeight;
const scrollTop = scrollContainer.scrollTop;
// Calculate visible range
const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - BUFFER_ROWS);
const endIndex = Math.min(visibleItems.length, Math.ceil((scrollTop + containerHeight) / ROW_HEIGHT) + BUFFER_ROWS);
// Skip if range hasn't changed significantly
if (startIndex === renderedRange.start && endIndex === renderedRange.end) return;
renderedRange = { start: startIndex, end: endIndex };
// Clear and rebuild
objectsTableBody.innerHTML = '';
// Add top spacer
initVirtualScrollElements();
topSpacer.querySelector('td').style.height = `${startIndex * ROW_HEIGHT}px`;
objectsTableBody.appendChild(topSpacer);
// Render visible rows
for (let i = startIndex; i < endIndex; i++) {
const item = visibleItems[i];
if (!item) continue;
let row;
if (item.type === 'folder') {
row = createFolderRow(item.path, item.displayKey);
@@ -2212,33 +2179,28 @@
row.dataset.virtualIndex = i;
objectsTableBody.appendChild(row);
}
// Add bottom spacer
const remainingRows = visibleItems.length - endIndex;
bottomSpacer.querySelector('td').style.height = `${remainingRows * ROW_HEIGHT}px`;
objectsTableBody.appendChild(bottomSpacer);
// Re-attach handlers to new rows
attachRowHandlers();
};
// Debounced scroll handler for virtual scrolling
let scrollTimeout = null;
const handleVirtualScroll = () => {
if (scrollTimeout) cancelAnimationFrame(scrollTimeout);
scrollTimeout = requestAnimationFrame(renderVirtualRows);
};
// Refresh the virtual list (after data changes or navigation)
const refreshVirtualList = () => {
visibleItems = computeVisibleItems();
renderedRange = { start: -1, end: -1 }; // Force re-render
renderedRange = { start: -1, end: -1 };
if (visibleItems.length === 0) {
if (allObjects.length === 0 && !hasMoreObjects) {
showEmptyState();
} else {
// Empty folder
objectsTableBody.innerHTML = `
<tr>
<td colspan="4" class="py-5">
@@ -2262,7 +2224,6 @@
updateFolderViewStatus();
};
// Update status bar
const updateFolderViewStatus = () => {
const folderViewStatusEl = document.getElementById('folder-view-status');
if (!folderViewStatusEl) return;
@@ -2277,8 +2238,6 @@
}
};
// ============== DATA LOADING ==============
const loadObjects = async (append = false) => {
if (isLoadingObjects) return;
isLoadingObjects = true;
@@ -2290,7 +2249,6 @@
allObjects = [];
}
// Show loading spinner when loading more
if (append && loadMoreSpinner) {
loadMoreSpinner.classList.remove('d-none');
}
@@ -2359,7 +2317,6 @@
updateLoadMoreButton();
}
// Refresh virtual scroll view
refreshVirtualList();
renderBreadcrumb(currentPrefix);
@@ -2379,7 +2336,6 @@
};
const attachRowHandlers = () => {
// Attach handlers to object rows
const objectRows = document.querySelectorAll('[data-object-row]');
objectRows.forEach(row => {
if (row.dataset.handlersAttached) return;
@@ -2405,14 +2361,12 @@
toggleRowSelection(row, selectCheckbox.checked);
});
// Restore selection state
if (selectedRows.has(row.dataset.key)) {
selectCheckbox.checked = true;
row.classList.add('table-active');
}
});
// Attach handlers to folder rows
const folderRows = document.querySelectorAll('.folder-row');
folderRows.forEach(row => {
if (row.dataset.handlersAttached) return;
@@ -2423,7 +2377,6 @@
const checkbox = row.querySelector('[data-folder-select]');
checkbox?.addEventListener('change', (e) => {
e.stopPropagation();
// Select all objects in this folder
const folderObjects = allObjects.filter(obj => obj.key.startsWith(folderPath));
folderObjects.forEach(obj => {
if (checkbox.checked) {
@@ -2450,31 +2403,26 @@
updateBulkDeleteState();
};
// Scroll container reference (needed for virtual scrolling)
const scrollSentinel = document.getElementById('scroll-sentinel');
const scrollContainer = document.querySelector('.objects-table-container');
const loadMoreBtn = document.getElementById('load-more-btn');
// Virtual scroll: listen to scroll events
if (scrollContainer) {
scrollContainer.addEventListener('scroll', handleVirtualScroll, { passive: true });
}
// Load More button click handler (fallback)
loadMoreBtn?.addEventListener('click', () => {
if (hasMoreObjects && !isLoadingObjects) {
loadObjects(true);
}
});
// Show/hide Load More button based on hasMoreObjects
function updateLoadMoreButton() {
if (loadMoreBtn) {
loadMoreBtn.classList.toggle('d-none', !hasMoreObjects);
}
}
// Auto-load more when near bottom (for loading all data)
if (scrollSentinel && scrollContainer) {
const containerObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
@@ -2484,7 +2432,7 @@
});
}, {
root: scrollContainer,
rootMargin: '500px', // Load more earlier for smoother experience
rootMargin: '500px',
threshold: 0
});
containerObserver.observe(scrollSentinel);
@@ -2503,7 +2451,6 @@
viewportObserver.observe(scrollSentinel);
}
// Page size selector (now controls batch size)
const pageSizeSelect = document.getElementById('page-size-select');
pageSizeSelect?.addEventListener('change', (e) => {
pageSize = parseInt(e.target.value, 10);
@@ -2669,14 +2616,11 @@
return tr;
};
// Instant client-side folder navigation (no server round-trip!)
const navigateToFolder = (prefix) => {
currentPrefix = prefix;
// Scroll to top when navigating
if (scrollContainer) scrollContainer.scrollTop = 0;
// Instant re-render from already-loaded data
refreshVirtualList();
renderBreadcrumb(prefix);
@@ -2710,9 +2654,9 @@
if (keyCell && currentPrefix) {
const displayName = obj.key.slice(currentPrefix.length);
keyCell.textContent = displayName;
keyCell.closest('.object-key').title = obj.key; // Full path in tooltip
keyCell.closest('.object-key').title = obj.key;
} else if (keyCell) {
keyCell.textContent = obj.key; // Reset to full key at root
keyCell.textContent = obj.key;
}
});
@@ -2887,7 +2831,6 @@
bulkDeleteConfirm.disabled = selectedCount === 0 || bulkDeleting;
}
if (selectAllCheckbox) {
// With virtual scrolling, count files in current folder from visibleItems
const filesInView = visibleItems.filter(item => item.type === 'file');
const total = filesInView.length;
const visibleSelectedCount = filesInView.filter(item => selectedRows.has(item.data.key)).length;
@@ -3524,9 +3467,6 @@
document.getElementById('object-search')?.addEventListener('input', (event) => {
currentFilterTerm = event.target.value.toLowerCase();
updateFilterWarning();
// Use the virtual scrolling system for filtering - it properly handles
// both folder view and flat view, and works with large object counts
refreshVirtualList();
});
@@ -3886,10 +3826,8 @@
selectAllCheckbox?.addEventListener('change', (event) => {
const shouldSelect = Boolean(event.target?.checked);
// Get all file items in the current view (works with virtual scrolling)
const filesInView = visibleItems.filter(item => item.type === 'file');
// Update selectedRows directly using object keys (not DOM elements)
filesInView.forEach(item => {
if (shouldSelect) {
selectedRows.set(item.data.key, item.data);
@@ -3898,12 +3836,10 @@
}
});
// Update folder checkboxes in DOM (folders are always rendered)
document.querySelectorAll('[data-folder-select]').forEach(cb => {
cb.checked = shouldSelect;
});
// Update any currently rendered object checkboxes
document.querySelectorAll('[data-object-row]').forEach((row) => {
const checkbox = row.querySelector('[data-object-select]');
if (checkbox) {
@@ -3917,7 +3853,6 @@
bulkDownloadButton?.addEventListener('click', async () => {
if (!bulkDownloadEndpoint) return;
// Use selectedRows which tracks all selected objects (not just rendered ones)
const selected = Array.from(selectedRows.keys());
if (selected.length === 0) return;
@@ -4085,7 +4020,6 @@
}
});
// Bucket name validation for replication setup
const targetBucketInput = document.getElementById('target_bucket');
const targetBucketFeedback = document.getElementById('target_bucket_feedback');
@@ -4120,7 +4054,6 @@
targetBucketInput?.addEventListener('input', updateBucketNameValidation);
targetBucketInput?.addEventListener('blur', updateBucketNameValidation);
// Prevent form submission if bucket name is invalid
const replicationForm = targetBucketInput?.closest('form');
replicationForm?.addEventListener('submit', (e) => {
const name = targetBucketInput.value.trim();
@@ -4133,7 +4066,6 @@
}
});
// Policy JSON validation and formatting
const formatPolicyBtn = document.getElementById('formatPolicyBtn');
const policyValidationStatus = document.getElementById('policyValidationStatus');
const policyValidBadge = document.getElementById('policyValidBadge');
@@ -4176,12 +4108,10 @@
policyTextarea.value = JSON.stringify(parsed, null, 2);
validatePolicyJson();
} catch (err) {
// Show error in validation
validatePolicyJson();
}
});
// Initialize policy validation on page load
if (policyTextarea && policyPreset?.value === 'custom') {
validatePolicyJson();
}

View File

@@ -133,7 +133,7 @@
const searchInput = document.getElementById('bucket-search');
const bucketItems = document.querySelectorAll('.bucket-item');
const noBucketsMsg = document.querySelector('.text-center.py-5'); // The "No buckets found" empty state
const noBucketsMsg = document.querySelector('.text-center.py-5');
if (searchInput) {
searchInput.addEventListener('input', (e) => {

View File

@@ -306,8 +306,7 @@
const data = Object.fromEntries(formData.entries());
resultDiv.innerHTML = '<div class="text-info"><span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Testing connection...</div>';
// Use AbortController to timeout client-side after 20 seconds
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 20000);
@@ -394,8 +393,6 @@
form.action = "{{ url_for('ui.delete_connection', connection_id='CONN_ID') }}".replace('CONN_ID', id);
});
// Check connection health for each connection in the table
// Uses staggered requests to avoid overwhelming the server
async function checkConnectionHealth(connectionId, statusEl) {
try {
const controller = new AbortController();
@@ -432,13 +429,11 @@
}
}
// Stagger health checks to avoid all requests at once
const connectionRows = document.querySelectorAll('tr[data-connection-id]');
connectionRows.forEach((row, index) => {
const connectionId = row.getAttribute('data-connection-id');
const statusEl = row.querySelector('.connection-status');
if (statusEl) {
// Stagger requests by 200ms each
setTimeout(() => checkConnectionHealth(connectionId, statusEl), index * 200);
}
});

View File

@@ -456,7 +456,6 @@
{{ super() }}
<script>
(function () {
// Auto-indent for JSON textareas
function setupJsonAutoIndent(textarea) {
if (!textarea) return;
@@ -468,61 +467,49 @@
const end = this.selectionEnd;
const value = this.value;
// Get the current line
const lineStart = value.lastIndexOf('\n', start - 1) + 1;
const currentLine = value.substring(lineStart, start);
// Calculate base indentation (leading whitespace of current line)
const indentMatch = currentLine.match(/^(\s*)/);
let indent = indentMatch ? indentMatch[1] : '';
// Check if the line ends with { or [ (should increase indent)
const trimmedLine = currentLine.trim();
const lastChar = trimmedLine.slice(-1);
// Check the character before cursor
const charBeforeCursor = value.substring(start - 1, start).trim();
let newIndent = indent;
let insertAfter = '';
if (lastChar === '{' || lastChar === '[') {
// Add extra indentation
newIndent = indent + ' ';
// Check if we need to add closing bracket on new line
const charAfterCursor = value.substring(start, start + 1).trim();
if ((lastChar === '{' && charAfterCursor === '}') ||
(lastChar === '[' && charAfterCursor === ']')) {
insertAfter = '\n' + indent;
}
} else if (lastChar === ',' || lastChar === ':') {
// Keep same indentation for continuation
newIndent = indent;
}
// Insert newline with proper indentation
const insertion = '\n' + newIndent + insertAfter;
const newValue = value.substring(0, start) + insertion + value.substring(end);
this.value = newValue;
// Set cursor position after the indentation
const newCursorPos = start + 1 + newIndent.length;
this.selectionStart = this.selectionEnd = newCursorPos;
// Trigger input event for any listeners
this.dispatchEvent(new Event('input', { bubbles: true }));
}
// Handle Tab key for indentation
if (e.key === 'Tab') {
e.preventDefault();
const start = this.selectionStart;
const end = this.selectionEnd;
if (e.shiftKey) {
// Outdent: remove 2 spaces from start of line
const lineStart = this.value.lastIndexOf('\n', start - 1) + 1;
const lineContent = this.value.substring(lineStart, start);
if (lineContent.startsWith(' ')) {
@@ -531,7 +518,6 @@
this.selectionStart = this.selectionEnd = Math.max(lineStart, start - 2);
}
} else {
// Indent: insert 2 spaces
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
this.selectionStart = this.selectionEnd = start + 2;
}
@@ -541,7 +527,6 @@
});
}
// Apply auto-indent to policy editor textareas
setupJsonAutoIndent(document.getElementById('policyEditorDocument'));
setupJsonAutoIndent(document.getElementById('createUserPolicies'));