diff --git a/weed/admin/static/js/modal-alerts.js b/weed/admin/static/js/modal-alerts.js new file mode 100644 index 000000000..612bc1961 --- /dev/null +++ b/weed/admin/static/js/modal-alerts.js @@ -0,0 +1,302 @@ +/** + * Modal Alerts - Bootstrap Modal replacement for native alert() and confirm() + * Fixes Chrome auto-dismiss issue with native dialogs + * + * Usage: + * showAlert('Message', 'success'); + * showConfirm('Delete this?', function() { }); + */ + +(function() { + 'use strict'; + + // Create and inject modal HTML into page if not already present + function ensureModalsExist() { + if (document.getElementById('globalAlertModal')) { + return; // Already exists + } + + const modalsHTML = ` + +
+ + + + + + + `; + + // Inject modals at end of body + document.body.insertAdjacentHTML('beforeend', modalsHTML); + } + + /** + * Show an alert message using Bootstrap modal + * @param {string} message - The message to display + * @param {string} type - Type: 'success', 'error', 'warning', 'info' (default: 'info') + * @param {string} title - Optional custom title + */ + window.showAlert = function(message, type, title) { + ensureModalsExist(); + + const modal = document.getElementById('globalAlertModal'); + const header = document.getElementById('globalAlertModalHeader'); + const titleEl = document.getElementById('globalAlertModalTitle'); + const bodyEl = document.getElementById('globalAlertModalBody'); + const iconEl = document.getElementById('globalAlertModalIcon'); + + // Configuration for different types + const types = { + 'success': { + title: 'Success', + icon: 'fa-check-circle', + headerClass: 'bg-success text-white', + btnClose: 'btn-close-white' + }, + 'error': { + title: 'Error', + icon: 'fa-exclamation-triangle', + headerClass: 'bg-danger text-white', + btnClose: 'btn-close-white' + }, + 'warning': { + title: 'Warning', + icon: 'fa-exclamation-circle', + headerClass: 'bg-warning text-dark', + btnClose: '' + }, + 'info': { + title: 'Notice', + icon: 'fa-info-circle', + headerClass: 'bg-info text-white', + btnClose: 'btn-close-white' + } + }; + + const config = types[type] || types['info']; + + // Update header styling + header.className = 'modal-header ' + config.headerClass; + const closeBtn = header.querySelector('.btn-close'); + closeBtn.className = 'btn-close ' + config.btnClose; + + // Update icon + iconEl.className = 'fas ' + config.icon + ' me-2'; + + // Update title + titleEl.textContent = title || config.title; + + // Update body - support HTML or text + if (message.includes('<')) { + bodyEl.innerHTML = message; + } else { + bodyEl.innerHTML = '' + escapeHtml(message) + '
'; + } + + // Show modal + const bsModal = new bootstrap.Modal(modal); + bsModal.show(); + }; + + /** + * Show a confirmation dialog using Bootstrap modal + * @param {string} message - The confirmation message + * @param {function} onConfirm - Callback function if user confirms + * @param {function} onCancel - Optional callback function if user cancels + * @param {string} title - Optional custom title + */ + window.showConfirm = function(message, onConfirm, onCancel, title) { + ensureModalsExist(); + + const modalEl = document.getElementById('globalConfirmModal'); + const bodyEl = document.getElementById('globalConfirmModalBody'); + const titleEl = document.getElementById('globalConfirmModalLabel').querySelector('span'); + const okBtn = document.getElementById('globalConfirmOkBtn'); + const cancelBtn = document.getElementById('globalConfirmCancelBtn'); + + // Set title if provided + if (title) { + if (titleEl) { + titleEl.textContent = title; + } else { + document.getElementById('globalConfirmModalLabel').insertAdjacentHTML('beforeend', '' + escapeHtml(title) + ''); + } + } + + // Set message + if (message.includes('<')) { + bodyEl.innerHTML = message; + } else { + bodyEl.innerHTML = '' + escapeHtml(message) + '
'; + } + + // Remove old event listeners by cloning buttons + const newOkBtn = okBtn.cloneNode(true); + const newCancelBtn = cancelBtn.cloneNode(true); + okBtn.parentNode.replaceChild(newOkBtn, okBtn); + cancelBtn.parentNode.replaceChild(newCancelBtn, cancelBtn); + + const modal = new bootstrap.Modal(modalEl); + + // Add event listeners + newOkBtn.addEventListener('click', function() { + modal.hide(); + if (typeof onConfirm === 'function') { + onConfirm(); + } + }); + + newCancelBtn.addEventListener('click', function() { + modal.hide(); + if (typeof onCancel === 'function') { + onCancel(); + } + }); + + modal.show(); + }; + + /** + * Show a delete confirmation dialog + * @param {string} itemName - Name of the item to delete + * @param {function} onConfirm - Callback function if user confirms deletion + * @param {string} message - Optional custom message (default: "Are you sure you want to delete this item?") + */ + window.showDeleteConfirm = function(itemName, onConfirm, message) { + ensureModalsExist(); + + const modalEl = document.getElementById('globalDeleteModal'); + const messageEl = document.getElementById('globalDeleteModalMessage'); + const itemNameEl = document.getElementById('globalDeleteModalItemName'); + const confirmBtn = document.getElementById('globalDeleteConfirmBtn'); + + // Set custom message if provided + if (message) { + messageEl.textContent = message; + } else { + messageEl.textContent = 'Are you sure you want to delete this item?'; + } + + // Set item name + itemNameEl.textContent = itemName; + + // Remove old event listener by cloning button + const newConfirmBtn = confirmBtn.cloneNode(true); + confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn); + + const modal = new bootstrap.Modal(modalEl); + + // Add new event listener + newConfirmBtn.addEventListener('click', function() { + modal.hide(); + if (typeof onConfirm === 'function') { + onConfirm(); + } + }); + + modal.show(); + }; + + /** + * Escape HTML to prevent XSS + */ + function escapeHtml(text) { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return text.replace(/[&<>"']/g, function(m) { return map[m]; }); + } + + // Auto-initialize on DOMContentLoaded + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', ensureModalsExist); + } else { + ensureModalsExist(); + } + + /** + * AUTOMATIC OVERRIDE of native alert() + * This makes ALL existing alert() calls automatically use Bootstrap modals + */ + window.alert = function(message) { + // Auto-detect message type from content + let type = 'info'; + const msgLower = (message || '').toLowerCase(); + + if (msgLower.includes('success') || msgLower.includes('created') || msgLower.includes('updated') || msgLower.includes('saved')) { + type = 'success'; + } else if (msgLower.includes('error') || msgLower.includes('failed') || msgLower.includes('invalid') || msgLower.includes('cannot')) { + type = 'error'; + } else if (msgLower.includes('warning') || msgLower.includes('please') || msgLower.includes('required')) { + type = 'warning'; + } + + showAlert(message, type); + }; + + console.log('Modal Alerts library loaded - native alert() overridden'); + console.log('For confirm(), use showConfirm() or showDeleteConfirm() instead of native confirm()'); +})(); diff --git a/weed/admin/view/app/cluster_ec_shards.templ b/weed/admin/view/app/cluster_ec_shards.templ index 19f6fd2d6..d7180f2bb 100644 --- a/weed/admin/view/app/cluster_ec_shards.templ +++ b/weed/admin/view/app/cluster_ec_shards.templ @@ -387,7 +387,7 @@ templ ClusterEcShards(data dash.ClusterEcShardsData) { // Get data from the button element (not the icon inside it) const button = event.target.closest('button'); const volumeId = button.getAttribute('data-volume-id'); - if (confirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`)) { + showConfirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`, function() { fetch(`/api/storage/volumes/${volumeId}/repair`, { method: 'POST', headers: { @@ -406,7 +406,7 @@ templ ClusterEcShards(data dash.ClusterEcShardsData) { .catch(error => { alert('Error: ' + error.message); }); - } + }); } } diff --git a/weed/admin/view/app/cluster_ec_shards_templ.go b/weed/admin/view/app/cluster_ec_shards_templ.go index b7c169d1e..31cf8f9bd 100644 --- a/weed/admin/view/app/cluster_ec_shards_templ.go +++ b/weed/admin/view/app/cluster_ec_shards_templ.go @@ -663,7 +663,7 @@ func ClusterEcShards(data dash.ClusterEcShardsData) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/cluster_ec_volumes.templ b/weed/admin/view/app/cluster_ec_volumes.templ index 6e94443ae..de0f32168 100644 --- a/weed/admin/view/app/cluster_ec_volumes.templ +++ b/weed/admin/view/app/cluster_ec_volumes.templ @@ -378,7 +378,7 @@ templ ClusterEcVolumes(data dash.ClusterEcVolumesData) { function repairVolume(event) { const volumeId = event.target.closest('button').getAttribute('data-volume-id'); - if (confirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`)) { + showConfirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`, function() { fetch(`/api/storage/ec-volumes/${volumeId}/repair`, { method: 'POST', headers: { @@ -402,7 +402,7 @@ templ ClusterEcVolumes(data dash.ClusterEcVolumesData) { .catch(error => { alert('Error: ' + error.message); }); - } + }); } } diff --git a/weed/admin/view/app/cluster_ec_volumes_templ.go b/weed/admin/view/app/cluster_ec_volumes_templ.go index 9103607a8..1c83c51ba 100644 --- a/weed/admin/view/app/cluster_ec_volumes_templ.go +++ b/weed/admin/view/app/cluster_ec_volumes_templ.go @@ -757,7 +757,7 @@ func ClusterEcVolumes(data dash.ClusterEcVolumesData) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/collection_details.templ b/weed/admin/view/app/collection_details.templ index 296839b93..46075613c 100644 --- a/weed/admin/view/app/collection_details.templ +++ b/weed/admin/view/app/collection_details.templ @@ -372,10 +372,10 @@ templ CollectionDetails(data dash.CollectionDetailsData) { // Repair EC Volume function repairEcVolume(event) { const volumeId = event.target.closest('button').getAttribute('data-volume-id'); - if (confirm(`Are you sure you want to repair missing shards for EC volume ${volumeId}?`)) { + showConfirm(`Are you sure you want to repair missing shards for EC volume ${volumeId}?`, function() { // TODO: Implement repair functionality alert('Repair functionality will be implemented soon.'); - } + }); } } \ No newline at end of file diff --git a/weed/admin/view/app/collection_details_templ.go b/weed/admin/view/app/collection_details_templ.go index f2ff0ab13..762f3c391 100644 --- a/weed/admin/view/app/collection_details_templ.go +++ b/weed/admin/view/app/collection_details_templ.go @@ -575,7 +575,7 @@ func CollectionDetails(data dash.CollectionDetailsData) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/file_browser.templ b/weed/admin/view/app/file_browser.templ index 6ae00b81b..a88f00488 100644 --- a/weed/admin/view/app/file_browser.templ +++ b/weed/admin/view/app/file_browser.templ @@ -365,9 +365,9 @@ templ FileBrowser(data dash.FileBrowserData) { showFileProperties(path); break; case 'delete': - if (confirm('Are you sure you want to delete "' + path + '"?')) { + showDeleteConfirm(path, function() { deleteFile(path); - } + }, 'Are you sure you want to delete this file?'); break; } }); diff --git a/weed/admin/view/app/file_browser_templ.go b/weed/admin/view/app/file_browser_templ.go index 4a66ff026..ca787195c 100644 --- a/weed/admin/view/app/file_browser_templ.go +++ b/weed/admin/view/app/file_browser_templ.go @@ -700,7 +700,7 @@ func FileBrowser(data dash.FileBrowserData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "\">Reclaims disk space by removing deleted files from volumes.
ConfigureRedistributes volumes across servers to optimize storage utilization.
ConfigureConverts volumes to erasure coded format for improved durability.
ConfigureReclaims disk space by removing deleted files from volumes.
ConfigureRedistributes volumes across servers to optimize storage utilization.
ConfigureConverts volumes to erasure coded format for improved durability.
Configure