Merge branch 'fix/chrome-dialog-modal-approach'

This commit is contained in:
Chris Lu committed 2026-01-25 23:21:23 -08:00
commit 759a6cd345
31 files changed
+353 -49

No files matched your search

+302
View File
@@ -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 = `
<!-- Global Alert Modal -->
<div class="modal fade" id="globalAlertModal" tabindex="-1" aria-labelledby="globalAlertModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header" id="globalAlertModalHeader">
<h5 class="modal-title" id="globalAlertModalLabel">
<i class="fas fa-info-circle me-2" id="globalAlertModalIcon"></i>
<span id="globalAlertModalTitle">Notice</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="globalAlertModalBody">
<!-- Message will be inserted here -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
<!-- Global Confirm Modal -->
<div class="modal fade" id="globalConfirmModal" tabindex="-1" aria-labelledby="globalConfirmModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-warning">
<h5 class="modal-title" id="globalConfirmModalLabel">
<i class="fas fa-question-circle me-2"></i>Confirm Action
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="globalConfirmModalBody">
<!-- Message will be inserted here -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="globalConfirmCancelBtn">Cancel</button>
<button type="button" class="btn btn-primary" id="globalConfirmOkBtn">OK</button>
</div>
</div>
</div>
</div>
<!-- Global Delete Confirm Modal -->
<div class="modal fade" id="globalDeleteModal" tabindex="-1" aria-labelledby="globalDeleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="globalDeleteModalLabel">
<i class="fas fa-exclamation-triangle me-2"></i>Confirm Delete
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="mb-2" id="globalDeleteModalMessage">Are you sure you want to delete this item?</p>
<p class="mb-0"><strong id="globalDeleteModalItemName"></strong></p>
<p class="text-muted small mt-2 mb-0">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="globalDeleteConfirmBtn">
<i class="fas fa-trash me-1"></i>Delete
</button>
</div>
</div>
</div>
</div>
`;
// 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 = '<p class="mb-0">' + escapeHtml(message) + '</p>';
}
// 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', '<span>' + escapeHtml(title) + '</span>');
}
}
// Set message
if (message.includes('<')) {
bodyEl.innerHTML = message;
} else {
bodyEl.innerHTML = '<p class="mb-0">' + escapeHtml(message) + '</p>';
}
// 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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
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()');
})();
+2 -2
View File
@@ -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);
});
}
});
}
</script>
}
@@ -663,7 +663,7 @@ func ClusterEcShards(data dash.ClusterEcShardsData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "<!-- JavaScript --><script>\n function sortBy(field) {\n const currentSort = \"{data.SortBy}\";\n const currentOrder = \"{data.SortOrder}\";\n let newOrder = 'asc';\n \n if (currentSort === field && currentOrder === 'asc') {\n newOrder = 'desc';\n }\n \n updateUrl({\n sortBy: field,\n sortOrder: newOrder,\n page: 1\n });\n }\n\n function goToPage(event) {\n // Get data from the link element (not any child elements)\n const link = event.target.closest('a');\n const page = link.getAttribute('data-page');\n updateUrl({ page: page });\n }\n\n function changePageSize() {\n const pageSize = document.getElementById('pageSizeSelect').value;\n updateUrl({ pageSize: pageSize, page: 1 });\n }\n\n function updateUrl(params) {\n const url = new URL(window.location);\n Object.keys(params).forEach(key => {\n if (params[key]) {\n url.searchParams.set(key, params[key]);\n } else {\n url.searchParams.delete(key);\n }\n });\n window.location.href = url.toString();\n }\n\n function exportEcShards() {\n const url = new URL('/api/storage/ec-shards/export', window.location.origin);\n const params = new URLSearchParams(window.location.search);\n params.forEach((value, key) => {\n url.searchParams.set(key, value);\n });\n window.open(url.toString(), '_blank');\n }\n\n function showShardDetails(event) {\n // Get data from the button element (not the icon inside it)\n const button = event.target.closest('button');\n const volumeId = button.getAttribute('data-volume-id');\n \n // Navigate to the EC volume details page\n window.location.href = `/storage/ec-volumes/${volumeId}`;\n }\n\n function repairVolume(event) {\n // Get data from the button element (not the icon inside it)\n const button = event.target.closest('button');\n const volumeId = button.getAttribute('data-volume-id');\n if (confirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`)) {\n fetch(`/api/storage/volumes/${volumeId}/repair`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n }\n })\n .then(response => response.json())\n .then(data => {\n if (data.success) {\n alert('Repair initiated successfully');\n location.reload();\n } else {\n alert('Failed to initiate repair: ' + data.error);\n }\n })\n .catch(error => {\n alert('Error: ' + error.message);\n });\n }\n }\n </script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "<!-- JavaScript --><script>\n function sortBy(field) {\n const currentSort = \"{data.SortBy}\";\n const currentOrder = \"{data.SortOrder}\";\n let newOrder = 'asc';\n \n if (currentSort === field && currentOrder === 'asc') {\n newOrder = 'desc';\n }\n \n updateUrl({\n sortBy: field,\n sortOrder: newOrder,\n page: 1\n });\n }\n\n function goToPage(event) {\n // Get data from the link element (not any child elements)\n const link = event.target.closest('a');\n const page = link.getAttribute('data-page');\n updateUrl({ page: page });\n }\n\n function changePageSize() {\n const pageSize = document.getElementById('pageSizeSelect').value;\n updateUrl({ pageSize: pageSize, page: 1 });\n }\n\n function updateUrl(params) {\n const url = new URL(window.location);\n Object.keys(params).forEach(key => {\n if (params[key]) {\n url.searchParams.set(key, params[key]);\n } else {\n url.searchParams.delete(key);\n }\n });\n window.location.href = url.toString();\n }\n\n function exportEcShards() {\n const url = new URL('/api/storage/ec-shards/export', window.location.origin);\n const params = new URLSearchParams(window.location.search);\n params.forEach((value, key) => {\n url.searchParams.set(key, value);\n });\n window.open(url.toString(), '_blank');\n }\n\n function showShardDetails(event) {\n // Get data from the button element (not the icon inside it)\n const button = event.target.closest('button');\n const volumeId = button.getAttribute('data-volume-id');\n \n // Navigate to the EC volume details page\n window.location.href = `/storage/ec-volumes/${volumeId}`;\n }\n\n function repairVolume(event) {\n // Get data from the button element (not the icon inside it)\n const button = event.target.closest('button');\n const volumeId = button.getAttribute('data-volume-id');\n showConfirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`, function() {\n fetch(`/api/storage/volumes/${volumeId}/repair`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n }\n })\n .then(response => response.json())\n .then(data => {\n if (data.success) {\n alert('Repair initiated successfully');\n location.reload();\n } else {\n alert('Failed to initiate repair: ' + data.error);\n }\n })\n .catch(error => {\n alert('Error: ' + error.message);\n });\n });\n }\n </script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+2 -2
View File
@@ -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);
});
}
});
}
</script>
}
@@ -757,7 +757,7 @@ func ClusterEcVolumes(data dash.ClusterEcVolumesData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<!-- JavaScript --><script>\n function sortBy(field) {\n const currentSort = new URLSearchParams(window.location.search).get('sort_by');\n const currentOrder = new URLSearchParams(window.location.search).get('sort_order') || 'asc';\n \n let newOrder = 'asc';\n if (currentSort === field && currentOrder === 'asc') {\n newOrder = 'desc';\n }\n \n updateUrl({\n sort_by: field,\n sort_order: newOrder,\n page: 1\n });\n }\n\n function goToPage(event) {\n event.preventDefault();\n const page = event.target.closest('a').getAttribute('data-page');\n updateUrl({ page: page });\n }\n\n function changePageSize(newPageSize) {\n updateUrl({ page_size: newPageSize, page: 1 });\n }\n\n function updateUrl(params) {\n const url = new URL(window.location);\n Object.keys(params).forEach(key => {\n if (params[key] != null) {\n url.searchParams.set(key, params[key]);\n } else {\n url.searchParams.delete(key);\n }\n });\n window.location.href = url.toString();\n }\n\n function showVolumeDetails(event) {\n const volumeId = event.target.closest('button').getAttribute('data-volume-id');\n window.location.href = `/storage/ec-volumes/${volumeId}`;\n }\n\n function repairVolume(event) {\n const volumeId = event.target.closest('button').getAttribute('data-volume-id');\n if (confirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`)) {\n fetch(`/api/storage/ec-volumes/${volumeId}/repair`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n }\n })\n .then(response => {\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n return response.json();\n })\n .then(data => {\n if (data && data.success) {\n alert('Repair initiated successfully');\n location.reload();\n } else {\n alert('Failed to initiate repair: ' + (data && data.error ? data.error : 'Unknown error'));\n }\n })\n .catch(error => {\n alert('Error: ' + error.message);\n });\n }\n }\n </script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<!-- JavaScript --><script>\n function sortBy(field) {\n const currentSort = new URLSearchParams(window.location.search).get('sort_by');\n const currentOrder = new URLSearchParams(window.location.search).get('sort_order') || 'asc';\n \n let newOrder = 'asc';\n if (currentSort === field && currentOrder === 'asc') {\n newOrder = 'desc';\n }\n \n updateUrl({\n sort_by: field,\n sort_order: newOrder,\n page: 1\n });\n }\n\n function goToPage(event) {\n event.preventDefault();\n const page = event.target.closest('a').getAttribute('data-page');\n updateUrl({ page: page });\n }\n\n function changePageSize(newPageSize) {\n updateUrl({ page_size: newPageSize, page: 1 });\n }\n\n function updateUrl(params) {\n const url = new URL(window.location);\n Object.keys(params).forEach(key => {\n if (params[key] != null) {\n url.searchParams.set(key, params[key]);\n } else {\n url.searchParams.delete(key);\n }\n });\n window.location.href = url.toString();\n }\n\n function showVolumeDetails(event) {\n const volumeId = event.target.closest('button').getAttribute('data-volume-id');\n window.location.href = `/storage/ec-volumes/${volumeId}`;\n }\n\n function repairVolume(event) {\n const volumeId = event.target.closest('button').getAttribute('data-volume-id');\n showConfirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`, function() {\n fetch(`/api/storage/ec-volumes/${volumeId}/repair`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n }\n })\n .then(response => {\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n return response.json();\n })\n .then(data => {\n if (data && data.success) {\n alert('Repair initiated successfully');\n location.reload();\n } else {\n alert('Failed to initiate repair: ' + (data && data.error ? data.error : 'Unknown error'));\n }\n })\n .catch(error => {\n alert('Error: ' + error.message);\n });\n });\n }\n </script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+2 -2
View File
@@ -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.');
}
});
}
</script>
}
@@ -575,7 +575,7 @@ func CollectionDetails(data dash.CollectionDetailsData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<script>\n\t\t// Sorting functionality\n\t\tfunction sortBy(field) {\n\t\t\tconst currentSort = new URLSearchParams(window.location.search).get('sort_by');\n\t\t\tconst currentOrder = new URLSearchParams(window.location.search).get('sort_order') || 'asc';\n\t\t\t\n\t\t\tlet newOrder = 'asc';\n\t\t\tif (currentSort === field && currentOrder === 'asc') {\n\t\t\t\tnewOrder = 'desc';\n\t\t\t}\n\t\t\t\n\t\t\tconst url = new URL(window.location);\n\t\t\turl.searchParams.set('sort_by', field);\n\t\t\turl.searchParams.set('sort_order', newOrder);\n\t\t\turl.searchParams.set('page', '1'); // Reset to first page\n\t\t\twindow.location.href = url.toString();\n\t\t}\n\n\t\t// Pagination functionality\n\t\tfunction goToPage(event) {\n\t\t\tevent.preventDefault();\n\t\t\tconst page = event.target.closest('a').getAttribute('data-page');\n\t\t\tconst url = new URL(window.location);\n\t\t\turl.searchParams.set('page', page);\n\t\t\twindow.location.href = url.toString();\n\t\t}\n\n\t\t// Page size functionality\n\t\tfunction changePageSize(newPageSize) {\n\t\t\tconst url = new URL(window.location);\n\t\t\turl.searchParams.set('page_size', newPageSize);\n\t\t\turl.searchParams.set('page', '1'); // Reset to first page when changing page size\n\t\t\twindow.location.href = url.toString();\n\t\t}\n\n\t\t// Volume details\n\t\tfunction showVolumeDetails(event) {\n\t\t\tconst volumeId = event.target.closest('button').getAttribute('data-volume-id');\n\t\t\tconst server = event.target.closest('button').getAttribute('data-server');\n\t\t\twindow.location.href = `/storage/volumes/${volumeId}/${server}`;\n\t\t}\n\n\t\t// EC Volume details\n\t\tfunction showEcVolumeDetails(event) {\n\t\t\tconst volumeId = event.target.closest('button').getAttribute('data-volume-id');\n\t\t\twindow.location.href = `/storage/ec-volumes/${volumeId}`;\n\t\t}\n\n\t\t// Repair EC Volume\n\t\tfunction repairEcVolume(event) {\n\t\t\tconst volumeId = event.target.closest('button').getAttribute('data-volume-id');\n\t\t\tif (confirm(`Are you sure you want to repair missing shards for EC volume ${volumeId}?`)) {\n\t\t\t\t// TODO: Implement repair functionality\n\t\t\t\talert('Repair functionality will be implemented soon.');\n\t\t\t}\n\t\t}\n\t</script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<script>\n\t\t// Sorting functionality\n\t\tfunction sortBy(field) {\n\t\t\tconst currentSort = new URLSearchParams(window.location.search).get('sort_by');\n\t\t\tconst currentOrder = new URLSearchParams(window.location.search).get('sort_order') || 'asc';\n\t\t\t\n\t\t\tlet newOrder = 'asc';\n\t\t\tif (currentSort === field && currentOrder === 'asc') {\n\t\t\t\tnewOrder = 'desc';\n\t\t\t}\n\t\t\t\n\t\t\tconst url = new URL(window.location);\n\t\t\turl.searchParams.set('sort_by', field);\n\t\t\turl.searchParams.set('sort_order', newOrder);\n\t\t\turl.searchParams.set('page', '1'); // Reset to first page\n\t\t\twindow.location.href = url.toString();\n\t\t}\n\n\t\t// Pagination functionality\n\t\tfunction goToPage(event) {\n\t\t\tevent.preventDefault();\n\t\t\tconst page = event.target.closest('a').getAttribute('data-page');\n\t\t\tconst url = new URL(window.location);\n\t\t\turl.searchParams.set('page', page);\n\t\t\twindow.location.href = url.toString();\n\t\t}\n\n\t\t// Page size functionality\n\t\tfunction changePageSize(newPageSize) {\n\t\t\tconst url = new URL(window.location);\n\t\t\turl.searchParams.set('page_size', newPageSize);\n\t\t\turl.searchParams.set('page', '1'); // Reset to first page when changing page size\n\t\t\twindow.location.href = url.toString();\n\t\t}\n\n\t\t// Volume details\n\t\tfunction showVolumeDetails(event) {\n\t\t\tconst volumeId = event.target.closest('button').getAttribute('data-volume-id');\n\t\t\tconst server = event.target.closest('button').getAttribute('data-server');\n\t\t\twindow.location.href = `/storage/volumes/${volumeId}/${server}`;\n\t\t}\n\n\t\t// EC Volume details\n\t\tfunction showEcVolumeDetails(event) {\n\t\t\tconst volumeId = event.target.closest('button').getAttribute('data-volume-id');\n\t\t\twindow.location.href = `/storage/ec-volumes/${volumeId}`;\n\t\t}\n\n\t\t// Repair EC Volume\n\t\tfunction repairEcVolume(event) {\n\t\t\tconst volumeId = event.target.closest('button').getAttribute('data-volume-id');\n showConfirm(`Are you sure you want to repair missing shards for EC volume ${volumeId}?`, function() {\n\t\t\t\t// TODO: Implement repair functionality\n\t\t\t\talert('Repair functionality will be implemented soon.');\n\t\t\t});\n\t\t}\n\t</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+2 -2
View File
@@ -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;
}
});
+1 -1
View File
@@ -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, "\"><!-- File List Preview --><div id=\"fileListPreview\" class=\"mb-3\" style=\"display: none;\"><label class=\"form-label\">Selected Files:</label><div id=\"selectedFilesList\" class=\"border rounded p-2 bg-light\"><!-- Files will be listed here --></div></div><!-- Upload Progress --><div class=\"mb-3\" id=\"uploadProgress\" style=\"display: none;\"><label class=\"form-label\">Upload Progress:</label><div class=\"progress mb-2\"><div class=\"progress-bar progress-bar-striped progress-bar-animated\" role=\"progressbar\" style=\"width: 0%\" aria-valuenow=\"0\" aria-valuemin=\"0\" aria-valuemax=\"100\">0%</div></div><div id=\"uploadStatus\" class=\"small text-muted\">Preparing upload...</div></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"submitUploadFile()\"><i class=\"fas fa-upload me-1\"></i>Upload Files</button></div></div></div></div><!-- JavaScript for file browser functionality --><script>\n\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t// Format permissions in the main table\n\t\tdocument.querySelectorAll('.permissions-display').forEach(element => {\n\t\t\tconst mode = element.getAttribute('data-mode');\n\t\t\tconst isDirectory = element.getAttribute('data-is-directory') === 'true';\n\t\t\tif (mode) {\n\t\t\t\telement.textContent = formatPermissions(mode, isDirectory);\n\t\t\t}\n\t\t});\n\t\t\n\t\t// Handle file browser action buttons (download, view, properties, delete)\n\t\tdocument.addEventListener('click', function(e) {\n\t\t\tconst button = e.target.closest('[data-action]');\n\t\t\tif (!button) return;\n\t\t\t\n\t\t\tconst action = button.getAttribute('data-action');\n\t\t\tconst path = button.getAttribute('data-path');\n\t\t\t\n\t\t\tif (!path) return;\n\t\t\t\n\t\t\tswitch(action) {\n\t\t\t\tcase 'download':\n\t\t\t\t\tdownloadFile(path);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'view':\n\t\t\t\t\tviewFile(path);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'properties':\n\t\t\t\t\tshowFileProperties(path);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'delete':\n\t\t\t\t\tif (confirm('Are you sure you want to delete \"' + path + '\"?')) {\n\t\t\t\t\t\tdeleteFile(path);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t});\n\t\t\n\t\t// Initialize file manager event handlers from admin.js\n\t\tif (typeof setupFileManagerEventHandlers === 'function') {\n\t\t\tsetupFileManagerEventHandlers();\n\t\t}\n\t});\n\t\n\t// File browser specific functions\n\tfunction downloadFile(path) {\n\t\t// Open download URL in new tab\n\t\twindow.open('/api/files/download?path=' + encodeURIComponent(path), '_blank');\n\t}\n\t\n\tfunction viewFile(path) {\n\t\t// Open file viewer in new tab\n\t\twindow.open('/api/files/view?path=' + encodeURIComponent(path), '_blank');\n\t}\n\t\n\tfunction showFileProperties(path) {\n\t\t// Fetch file properties and show in modal\n\t\tfetch('/api/files/properties?path=' + encodeURIComponent(path))\n\t\t\t.then(response => response.json())\n\t\t\t.then(data => {\n\t\t\t\tif (data.error) {\n\t\t\t\t\talert('Error loading file properties: ' + data.error);\n\t\t\t\t} else {\n\t\t\t\t\tdisplayFileProperties(data);\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(error => {\n\t\t\t\tconsole.error('Error fetching file properties:', error);\n\t\t\t\talert('Error loading file properties: ' + error.message);\n\t\t\t});\n\t}\n\t\n\tfunction displayFileProperties(data) {\n\t\t// Create a comprehensive modal for file properties\n\t\tconst modalHtml = '<div class=\"modal fade\" id=\"filePropertiesModal\" tabindex=\"-1\">' +\n\t\t\t'<div class=\"modal-dialog modal-lg\">' +\n\t\t\t'<div class=\"modal-content\">' +\n\t\t\t'<div class=\"modal-header\">' +\n\t\t\t'<h5 class=\"modal-title\"><i class=\"fas fa-info-circle me-2\"></i>Properties: ' + (data.name || 'Unknown') + '</h5>' +\n\t\t\t'<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button>' +\n\t\t\t'</div>' +\n\t\t\t'<div class=\"modal-body\">' +\n\t\t\tcreateFilePropertiesContent(data) +\n\t\t\t'</div>' +\n\t\t\t'<div class=\"modal-footer\">' +\n\t\t\t'<button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Close</button>' +\n\t\t\t'</div>' +\n\t\t\t'</div>' +\n\t\t\t'</div>' +\n\t\t\t'</div>';\n\t\t\n\t\t// Remove existing modal if present\n\t\tconst existingModal = document.getElementById('filePropertiesModal');\n\t\tif (existingModal) {\n\t\t\texistingModal.remove();\n\t\t}\n\t\t\n\t\t// Add modal to body and show\n\t\tdocument.body.insertAdjacentHTML('beforeend', modalHtml);\n\t\tconst modal = new bootstrap.Modal(document.getElementById('filePropertiesModal'));\n\t\tmodal.show();\n\t\t\n\t\t// Remove modal when hidden\n\t\tdocument.getElementById('filePropertiesModal').addEventListener('hidden.bs.modal', function() {\n\t\t\tthis.remove();\n\t\t});\n\t}\n\t\n\tfunction createFilePropertiesContent(data) {\nLine truncated
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "\"><!-- File List Preview --><div id=\"fileListPreview\" class=\"mb-3\" style=\"display: none;\"><label class=\"form-label\">Selected Files:</label><div id=\"selectedFilesList\" class=\"border rounded p-2 bg-light\"><!-- Files will be listed here --></div></div><!-- Upload Progress --><div class=\"mb-3\" id=\"uploadProgress\" style=\"display: none;\"><label class=\"form-label\">Upload Progress:</label><div class=\"progress mb-2\"><div class=\"progress-bar progress-bar-striped progress-bar-animated\" role=\"progressbar\" style=\"width: 0%\" aria-valuenow=\"0\" aria-valuemin=\"0\" aria-valuemax=\"100\">0%</div></div><div id=\"uploadStatus\" class=\"small text-muted\">Preparing upload...</div></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"submitUploadFile()\"><i class=\"fas fa-upload me-1\"></i>Upload Files</button></div></div></div></div><!-- JavaScript for file browser functionality --><script>\n\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t// Format permissions in the main table\n\t\tdocument.querySelectorAll('.permissions-display').forEach(element => {\n\t\t\tconst mode = element.getAttribute('data-mode');\n\t\t\tconst isDirectory = element.getAttribute('data-is-directory') === 'true';\n\t\t\tif (mode) {\n\t\t\t\telement.textContent = formatPermissions(mode, isDirectory);\n\t\t\t}\n\t\t});\n\t\t\n\t\t// Handle file browser action buttons (download, view, properties, delete)\n\t\tdocument.addEventListener('click', function(e) {\n\t\t\tconst button = e.target.closest('[data-action]');\n\t\t\tif (!button) return;\n\t\t\t\n\t\t\tconst action = button.getAttribute('data-action');\n\t\t\tconst path = button.getAttribute('data-path');\n\t\t\t\n\t\t\tif (!path) return;\n\t\t\t\n\t\t\tswitch(action) {\n\t\t\t\tcase 'download':\n\t\t\t\t\tdownloadFile(path);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'view':\n\t\t\t\t\tviewFile(path);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'properties':\n\t\t\t\t\tshowFileProperties(path);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'delete':\n\t\t\t\t\tshowDeleteConfirm(path, function() {\n\t\t\t\t\t\tdeleteFile(path);\n\t\t\t\t\t}, 'Are you sure you want to delete this file?');\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t});\n\t\t\n\t\t// Initialize file manager event handlers from admin.js\n\t\tif (typeof setupFileManagerEventHandlers === 'function') {\n\t\t\tsetupFileManagerEventHandlers();\n\t\t}\n\t});\n\t\n\t// File browser specific functions\n\tfunction downloadFile(path) {\n\t\t// Open download URL in new tab\n\t\twindow.open('/api/files/download?path=' + encodeURIComponent(path), '_blank');\n\t}\n\t\n\tfunction viewFile(path) {\n\t\t// Open file viewer in new tab\n\t\twindow.open('/api/files/view?path=' + encodeURIComponent(path), '_blank');\n\t}\n\t\n\tfunction showFileProperties(path) {\n\t\t// Fetch file properties and show in modal\n\t\tfetch('/api/files/properties?path=' + encodeURIComponent(path))\n\t\t\t.then(response => response.json())\n\t\t\t.then(data => {\n\t\t\t\tif (data.error) {\n\t\t\t\t\talert('Error loading file properties: ' + data.error);\n\t\t\t\t} else {\n\t\t\t\t\tdisplayFileProperties(data);\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(error => {\n\t\t\t\tconsole.error('Error fetching file properties:', error);\n\t\t\t\talert('Error loading file properties: ' + error.message);\n\t\t\t});\n\t}\n\t\n\tfunction displayFileProperties(data) {\n\t\t// Create a comprehensive modal for file properties\n\t\tconst modalHtml = '<div class=\"modal fade\" id=\"filePropertiesModal\" tabindex=\"-1\">' +\n\t\t\t'<div class=\"modal-dialog modal-lg\">' +\n\t\t\t'<div class=\"modal-content\">' +\n\t\t\t'<div class=\"modal-header\">' +\n\t\t\t'<h5 class=\"modal-title\"><i class=\"fas fa-info-circle me-2\"></i>Properties: ' + (data.name || 'Unknown') + '</h5>' +\n\t\t\t'<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button>' +\n\t\t\t'</div>' +\n\t\t\t'<div class=\"modal-body\">' +\n\t\t\tcreateFilePropertiesContent(data) +\n\t\t\t'</div>' +\n\t\t\t'<div class=\"modal-footer\">' +\n\t\t\t'<button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Close</button>' +\n\t\t\t'</div>' +\n\t\t\t'</div>' +\n\t\t\t'</div>' +\n\t\t\t'</div>';\n\t\t\n\t\t// Remove existing modal if present\n\t\tconst existingModal = document.getElementById('filePropertiesModal');\n\t\tif (existingModal) {\n\t\t\texistingModal.remove();\n\t\t}\n\t\t\n\t\t// Add modal to body and show\n\t\tdocument.body.insertAdjacentHTML('beforeend', modalHtml);\n\t\tconst modal = new bootstrap.Modal(document.getElementById('filePropertiesModal'));\n\t\tmodal.show();\n\t\t\n\t\t// Remove modal when hidden\n\t\tdocument.getElementById('filePropertiesModal').addEventListener('hidden.bs.modal', function() {\n\t\t\tthis.remove();\n\t\t});\n\t}\n\t\n\tfunction createFilePropertiesLine truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+2 -2
View File
@@ -251,7 +251,7 @@ templ MaintenanceConfig(data *maintenance.MaintenanceConfigData) {
}
function resetToDefaults() {
if (confirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.')) {
showConfirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.', function() {
// Reset form to defaults (matching DefaultMaintenanceConfig values)
document.getElementById('enabled').checked = false;
document.getElementById('scanInterval').value = '30';
@@ -261,7 +261,7 @@ templ MaintenanceConfig(data *maintenance.MaintenanceConfigData) {
document.getElementById('maxRetries').value = '3';
document.getElementById('retryDelay').value = '15';
document.getElementById('taskRetention').value = '7';
}
});
}
</script>
}
@@ -179,7 +179,7 @@ templ MaintenanceConfigSchema(data *maintenance.MaintenanceConfigData, schema *m
}
function resetToDefaults() {
if (confirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.')) {
showConfirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.', function() {
fetch('/maintenance/config/defaults', {
method: 'POST',
headers: {
@@ -199,7 +199,7 @@ templ MaintenanceConfigSchema(data *maintenance.MaintenanceConfigData, schema *m
console.error('Error:', error);
alert('Error resetting configuration: ' + error.message);
});
}
});
}
</script>
}
@@ -46,7 +46,7 @@ func MaintenanceConfigSchema(data *maintenance.MaintenanceConfigData, schema *ma
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"d-flex gap-2\"><button type=\"button\" class=\"btn btn-primary\" onclick=\"saveConfiguration()\"><i class=\"fas fa-save me-1\"></i> Save Configuration</button> <button type=\"button\" class=\"btn btn-secondary\" onclick=\"resetToDefaults()\"><i class=\"fas fa-undo me-1\"></i> Reset to Defaults</button></div></form></div></div></div></div><!-- Task Configuration Cards --><div class=\"row mt-4\"><div class=\"col-md-4\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"fas fa-broom me-2\"></i> Volume Vacuum</h5></div><div class=\"card-body\"><p class=\"card-text\">Reclaims disk space by removing deleted files from volumes.</p><a href=\"/maintenance/config/vacuum\" class=\"btn btn-primary\">Configure</a></div></div></div><div class=\"col-md-4\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"fas fa-balance-scale me-2\"></i> Volume Balance</h5></div><div class=\"card-body\"><p class=\"card-text\">Redistributes volumes across servers to optimize storage utilization.</p><a href=\"/maintenance/config/balance\" class=\"btn btn-primary\">Configure</a></div></div></div><div class=\"col-md-4\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"fas fa-shield-alt me-2\"></i> Erasure Coding</h5></div><div class=\"card-body\"><p class=\"card-text\">Converts volumes to erasure coded format for improved durability.</p><a href=\"/maintenance/config/erasure_coding\" class=\"btn btn-primary\">Configure</a></div></div></div></div></div><script>\n function saveConfiguration() {\n const form = document.getElementById('maintenanceConfigForm');\n const formData = new FormData(form);\n \n // Convert form data to JSON, handling interval fields specially\n const config = {};\n \n for (let [key, value] of formData.entries()) {\n if (key.endsWith('_value')) {\n // This is an interval value part\n const baseKey = key.replace('_value', '');\n const unitKey = baseKey + '_unit';\n const unitValue = formData.get(unitKey);\n \n if (unitValue) {\n // Convert to seconds based on unit\n const numValue = parseInt(value) || 0;\n let seconds = numValue;\n switch(unitValue) {\n case 'minutes':\n seconds = numValue * 60;\n break;\n case 'hours':\n seconds = numValue * 3600;\n break;\n case 'days':\n seconds = numValue * 24 * 3600;\n break;\n }\n config[baseKey] = seconds;\n }\n } else if (key.endsWith('_unit')) {\n // Skip unit keys - they're handled with their corresponding value\n continue;\n } else {\n // Regular field\n if (form.querySelector(`[name=\"${key}\"]`).type === 'checkbox') {\n config[key] = form.querySelector(`[name=\"${key}\"]`).checked;\n } else {\n const numValue = parseFloat(value);\n config[key] = isNaN(numValue) ? value : numValue;\n }\n }\n }\n\n fetch('/api/maintenance/config', {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(config)\n })\n .then(response => {\n if (response.status === 401) {\n alert('Authentication required. Please log in first.');\n window.location.href = '/login';\n return;\n }\n return response.json();\n })\n .then(data => {\n if (!data) return; // Skip if redirected to login\n if (data.success) {\n alert('Configuration saved successfully!');\n location.reload();\n } else {\n alert('Error saving configuration: ' + (data.error || 'Unknown error'));\n }\n })\n .catch(error => {\n console.error('Error:', error);\n alert('Error saving configuration: ' + error.message);\n });\n }\n\n function resetToDefaults() {\n if (confirm('Are you sure you want to reset to default configuration? This will overwrite youLine truncated
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"d-flex gap-2\"><button type=\"button\" class=\"btn btn-primary\" onclick=\"saveConfiguration()\"><i class=\"fas fa-save me-1\"></i> Save Configuration</button> <button type=\"button\" class=\"btn btn-secondary\" onclick=\"resetToDefaults()\"><i class=\"fas fa-undo me-1\"></i> Reset to Defaults</button></div></form></div></div></div></div><!-- Task Configuration Cards --><div class=\"row mt-4\"><div class=\"col-md-4\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"fas fa-broom me-2\"></i> Volume Vacuum</h5></div><div class=\"card-body\"><p class=\"card-text\">Reclaims disk space by removing deleted files from volumes.</p><a href=\"/maintenance/config/vacuum\" class=\"btn btn-primary\">Configure</a></div></div></div><div class=\"col-md-4\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"fas fa-balance-scale me-2\"></i> Volume Balance</h5></div><div class=\"card-body\"><p class=\"card-text\">Redistributes volumes across servers to optimize storage utilization.</p><a href=\"/maintenance/config/balance\" class=\"btn btn-primary\">Configure</a></div></div></div><div class=\"col-md-4\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"fas fa-shield-alt me-2\"></i> Erasure Coding</h5></div><div class=\"card-body\"><p class=\"card-text\">Converts volumes to erasure coded format for improved durability.</p><a href=\"/maintenance/config/erasure_coding\" class=\"btn btn-primary\">Configure</a></div></div></div></div></div><script>\n function saveConfiguration() {\n const form = document.getElementById('maintenanceConfigForm');\n const formData = new FormData(form);\n \n // Convert form data to JSON, handling interval fields specially\n const config = {};\n \n for (let [key, value] of formData.entries()) {\n if (key.endsWith('_value')) {\n // This is an interval value part\n const baseKey = key.replace('_value', '');\n const unitKey = baseKey + '_unit';\n const unitValue = formData.get(unitKey);\n \n if (unitValue) {\n // Convert to seconds based on unit\n const numValue = parseInt(value) || 0;\n let seconds = numValue;\n switch(unitValue) {\n case 'minutes':\n seconds = numValue * 60;\n break;\n case 'hours':\n seconds = numValue * 3600;\n break;\n case 'days':\n seconds = numValue * 24 * 3600;\n break;\n }\n config[baseKey] = seconds;\n }\n } else if (key.endsWith('_unit')) {\n // Skip unit keys - they're handled with their corresponding value\n continue;\n } else {\n // Regular field\n if (form.querySelector(`[name=\"${key}\"]`).type === 'checkbox') {\n config[key] = form.querySelector(`[name=\"${key}\"]`).checked;\n } else {\n const numValue = parseFloat(value);\n config[key] = isNaN(numValue) ? value : numValue;\n }\n }\n }\n\n fetch('/api/maintenance/config', {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(config)\n })\n .then(response => {\n if (response.status === 401) {\n alert('Authentication required. Please log in first.');\n window.location.href = '/login';\n return;\n }\n return response.json();\n })\n .then(data => {\n if (!data) return; // Skip if redirected to login\n if (data.success) {\n alert('Configuration saved successfully!');\n location.reload();\n } else {\n alert('Error saving configuration: ' + (data.error || 'Unknown error'));\n }\n })\n .catch(error => {\n console.error('Error:', error);\n alert('Error saving configuration: ' + error.message);\n });\n }\n\n function resetToDefaults() {\n showConfirm('Are you sure you want to reset to default configuration? This will overwrite youLine truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -273,7 +273,7 @@ func MaintenanceConfig(data *maintenance.MaintenanceConfigData) templ.Component
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</p></div></div></div></div></div></div></div></div><script>\n function saveConfiguration() {\n // First, get current configuration to preserve existing values\n fetch('/api/maintenance/config')\n .then(response => response.json())\n .then(currentConfig => {\n // Update only the fields from the form\n const updatedConfig = {\n ...currentConfig.config, // Preserve existing config\n enabled: document.getElementById('enabled').checked,\n scan_interval_seconds: parseInt(document.getElementById('scanInterval').value) * 60, // Convert to seconds\n worker_timeout_seconds: parseInt(document.getElementById('workerTimeout').value) * 60, // Convert to seconds\n task_timeout_seconds: parseInt(document.getElementById('taskTimeout').value) * 3600, // Convert to seconds\n retry_delay_seconds: parseInt(document.getElementById('retryDelay').value) * 60, // Convert to seconds\n max_retries: parseInt(document.getElementById('maxRetries').value),\n task_retention_seconds: parseInt(document.getElementById('taskRetention').value) * 24 * 3600, // Convert to seconds\n policy: {\n ...currentConfig.config.policy, // Preserve existing policy\n global_max_concurrent: parseInt(document.getElementById('globalMaxConcurrent').value)\n }\n };\n\n // Send the updated configuration\n return fetch('/api/maintenance/config', {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updatedConfig)\n });\n })\n .then(response => response.json())\n .then(data => {\n if (data.success) {\n alert('Configuration saved successfully');\n location.reload(); // Reload to show updated values\n } else {\n alert('Failed to save configuration: ' + (data.error || 'Unknown error'));\n }\n })\n .catch(error => {\n alert('Error: ' + error.message);\n });\n }\n\n function resetToDefaults() {\n if (confirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.')) {\n // Reset form to defaults (matching DefaultMaintenanceConfig values)\n document.getElementById('enabled').checked = false;\n document.getElementById('scanInterval').value = '30';\n document.getElementById('workerTimeout').value = '5';\n document.getElementById('taskTimeout').value = '2';\n document.getElementById('globalMaxConcurrent').value = '4';\n document.getElementById('maxRetries').value = '3';\n document.getElementById('retryDelay').value = '15';\n document.getElementById('taskRetention').value = '7';\n }\n }\n </script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</p></div></div></div></div></div></div></div></div><script>\n function saveConfiguration() {\n // First, get current configuration to preserve existing values\n fetch('/api/maintenance/config')\n .then(response => response.json())\n .then(currentConfig => {\n // Update only the fields from the form\n const updatedConfig = {\n ...currentConfig.config, // Preserve existing config\n enabled: document.getElementById('enabled').checked,\n scan_interval_seconds: parseInt(document.getElementById('scanInterval').value) * 60, // Convert to seconds\n worker_timeout_seconds: parseInt(document.getElementById('workerTimeout').value) * 60, // Convert to seconds\n task_timeout_seconds: parseInt(document.getElementById('taskTimeout').value) * 3600, // Convert to seconds\n retry_delay_seconds: parseInt(document.getElementById('retryDelay').value) * 60, // Convert to seconds\n max_retries: parseInt(document.getElementById('maxRetries').value),\n task_retention_seconds: parseInt(document.getElementById('taskRetention').value) * 24 * 3600, // Convert to seconds\n policy: {\n ...currentConfig.config.policy, // Preserve existing policy\n global_max_concurrent: parseInt(document.getElementById('globalMaxConcurrent').value)\n }\n };\n\n // Send the updated configuration\n return fetch('/api/maintenance/config', {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updatedConfig)\n });\n })\n .then(response => response.json())\n .then(data => {\n if (data.success) {\n alert('Configuration saved successfully');\n location.reload(); // Reload to show updated values\n } else {\n alert('Failed to save configuration: ' + (data.error || 'Unknown error'));\n }\n })\n .catch(error => {\n alert('Error: ' + error.message);\n });\n }\n\n function resetToDefaults() {\n showConfirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.', function() {\n // Reset form to defaults (matching DefaultMaintenanceConfig values)\n document.getElementById('enabled').checked = false;\n document.getElementById('scanInterval').value = '30';\n document.getElementById('workerTimeout').value = '5';\n document.getElementById('taskTimeout').value = '2';\n document.getElementById('globalMaxConcurrent').value = '4';\n document.getElementById('maxRetries').value = '3';\n document.getElementById('retryDelay').value = '15';\n document.getElementById('taskRetention').value = '7';\n });\n }\n </script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -302,7 +302,7 @@ templ MaintenanceWorkers(data *dash.MaintenanceWorkersData) {
function pauseWorker(event) {
const workerID = event.target.closest('button').getAttribute('data-worker-id');
if (confirm('Are you sure you want to pause this worker?')) {
showConfirm('Are you sure you want to pause this worker?', function() {
fetch('/api/maintenance/workers/' + workerID + '/pause', {
method: 'POST'
})
@@ -318,7 +318,7 @@ templ MaintenanceWorkers(data *dash.MaintenanceWorkersData) {
console.error('Error pausing worker:', error);
alert('Failed to pause worker');
});
}
});
}
function formatDuration(nanoseconds) {
@@ -390,7 +390,7 @@ func MaintenanceWorkers(data *dash.MaintenanceWorkersData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div></div></div></div></div><!-- Worker Details Modal --><div class=\"modal fade\" id=\"workerDetailsModal\" tabindex=\"-1\" aria-labelledby=\"workerDetailsModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog modal-lg\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"workerDetailsModalLabel\">Worker Details</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\" id=\"workerDetailsContent\"><!-- Content will be loaded dynamically --></div></div></div></div><script>\n function showWorkerDetails(event) {\n const workerID = event.target.closest('button').getAttribute('data-worker-id');\n \n // Show modal\n var modal = new bootstrap.Modal(document.getElementById('workerDetailsModal'));\n \n // Load worker details\n fetch('/api/maintenance/workers/' + workerID)\n .then(response => response.json())\n .then(data => {\n const content = document.getElementById('workerDetailsContent');\n content.innerHTML = '<div class=\"row\">' +\n '<div class=\"col-md-6\">' +\n '<h6>Worker Information</h6>' +\n '<ul class=\"list-unstyled\">' +\n '<li><strong>ID:</strong> ' + data.worker.id + '</li>' +\n '<li><strong>Address:</strong> ' + data.worker.address + '</li>' +\n '<li><strong>Status:</strong> ' + data.worker.status + '</li>' +\n '<li><strong>Max Concurrent:</strong> ' + data.worker.max_concurrent + '</li>' +\n '<li><strong>Current Load:</strong> ' + data.worker.current_load + '</li>' +\n '</ul>' +\n '</div>' +\n '<div class=\"col-md-6\">' +\n '<h6>Performance Metrics</h6>' +\n '<ul class=\"list-unstyled\">' +\n '<li><strong>Tasks Completed:</strong> ' + data.performance.tasks_completed + '</li>' +\n '<li><strong>Tasks Failed:</strong> ' + data.performance.tasks_failed + '</li>' +\n '<li><strong>Success Rate:</strong> ' + data.performance.success_rate.toFixed(1) + '%</li>' +\n '<li><strong>Average Task Time:</strong> ' + formatDuration(data.performance.average_task_time) + '</li>' +\n '<li><strong>Uptime:</strong> ' + formatDuration(data.performance.uptime) + '</li>' +\n '</ul>' +\n '</div>' +\n '</div>' +\n '<hr>' +\n '<h6>Current Tasks</h6>' +\n (data.current_tasks.length === 0 ? \n '<p class=\"text-muted\">No current tasks</p>' :\n data.current_tasks.map(task => \n '<div class=\"card mb-2\">' +\n '<div class=\"card-body py-2\">' +\n '<div class=\"d-flex justify-content-between\">' +\n '<span><strong>' + task.type + '</strong> - Volume ' + task.volume_id + '</span>' +\n '<span class=\"badge bg-info\">' + task.status + '</span>' +\n '</div>' +\n '<small class=\"text-muted\">' + task.reason + '</small>' +\n '</div>' +\n '</div>'\n ).join('')\n );\n modal.show();\n })\n .catch(error => {\n console.error('Error loading worker details:', error);\n const content = document.getElementById('workerDetailsContent');\n content.innerHTML = '<div class=\"alert alert-danger\">Failed to load worker details</div>';\n modal.show();\n });\n }\n\n function pauseWorker(event) {\n const workerID = event.target.closest('button').getAttribute('data-worker-id');\n \n if (confirm('Are you sure you want to pause this worker?')) {\n fetch('/api/maintenance/workers/' + workerID + '/pause', {\n method: 'POST'\n })\n .then(response => response.json())\n .then(data => {\n if (data.success) {\n location.reload();\n } else {\n alert('Failed to pause worker: ' + data.error);\n }\n })\n .catch(error => {\n console.error('Error pausing worker:', error);\n alert('Failed to pause worker');\n });\n }\n }\n\n function formatDuration(nanoseconds) {\n Line truncated
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div></div></div></div></div><!-- Worker Details Modal --><div class=\"modal fade\" id=\"workerDetailsModal\" tabindex=\"-1\" aria-labelledby=\"workerDetailsModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog modal-lg\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"workerDetailsModalLabel\">Worker Details</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\" id=\"workerDetailsContent\"><!-- Content will be loaded dynamically --></div></div></div></div><script>\n function showWorkerDetails(event) {\n const workerID = event.target.closest('button').getAttribute('data-worker-id');\n \n // Show modal\n var modal = new bootstrap.Modal(document.getElementById('workerDetailsModal'));\n \n // Load worker details\n fetch('/api/maintenance/workers/' + workerID)\n .then(response => response.json())\n .then(data => {\n const content = document.getElementById('workerDetailsContent');\n content.innerHTML = '<div class=\"row\">' +\n '<div class=\"col-md-6\">' +\n '<h6>Worker Information</h6>' +\n '<ul class=\"list-unstyled\">' +\n '<li><strong>ID:</strong> ' + data.worker.id + '</li>' +\n '<li><strong>Address:</strong> ' + data.worker.address + '</li>' +\n '<li><strong>Status:</strong> ' + data.worker.status + '</li>' +\n '<li><strong>Max Concurrent:</strong> ' + data.worker.max_concurrent + '</li>' +\n '<li><strong>Current Load:</strong> ' + data.worker.current_load + '</li>' +\n '</ul>' +\n '</div>' +\n '<div class=\"col-md-6\">' +\n '<h6>Performance Metrics</h6>' +\n '<ul class=\"list-unstyled\">' +\n '<li><strong>Tasks Completed:</strong> ' + data.performance.tasks_completed + '</li>' +\n '<li><strong>Tasks Failed:</strong> ' + data.performance.tasks_failed + '</li>' +\n '<li><strong>Success Rate:</strong> ' + data.performance.success_rate.toFixed(1) + '%</li>' +\n '<li><strong>Average Task Time:</strong> ' + formatDuration(data.performance.average_task_time) + '</li>' +\n '<li><strong>Uptime:</strong> ' + formatDuration(data.performance.uptime) + '</li>' +\n '</ul>' +\n '</div>' +\n '</div>' +\n '<hr>' +\n '<h6>Current Tasks</h6>' +\n (data.current_tasks.length === 0 ? \n '<p class=\"text-muted\">No current tasks</p>' :\n data.current_tasks.map(task => \n '<div class=\"card mb-2\">' +\n '<div class=\"card-body py-2\">' +\n '<div class=\"d-flex justify-content-between\">' +\n '<span><strong>' + task.type + '</strong> - Volume ' + task.volume_id + '</span>' +\n '<span class=\"badge bg-info\">' + task.status + '</span>' +\n '</div>' +\n '<small class=\"text-muted\">' + task.reason + '</small>' +\n '</div>' +\n '</div>'\n ).join('')\n );\n modal.show();\n })\n .catch(error => {\n console.error('Error loading worker details:', error);\n const content = document.getElementById('workerDetailsContent');\n content.innerHTML = '<div class=\"alert alert-danger\">Failed to load worker details</div>';\n modal.show();\n });\n }\n\n function pauseWorker(event) {\n const workerID = event.target.closest('button').getAttribute('data-worker-id');\n \n showConfirm('Are you sure you want to pause this worker?', function() {\n fetch('/api/maintenance/workers/' + workerID + '/pause', {\n method: 'POST'\n })\n .then(response => response.json())\n .then(data => {\n if (data.success) {\n location.reload();\n } else {\n alert('Failed to pause worker: ' + data.error);\n }\n })\n .catch(error => {\n console.error('Error pausing worker:', error);\n alert('Failed to pause worker');\n });\n });\n }\n\n function formatDuration(nanoseLine truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+4 -4
View File
@@ -802,7 +802,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
// Delete user function
async function deleteUser(username) {
if (confirm(`Are you sure you want to delete user "${username}"? This action cannot be undone.`)) {
showDeleteConfirm(username, async function() {
try {
const response = await fetch(`/api/users/${username}`, {
method: 'DELETE'
@@ -819,7 +819,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
console.error('Error deleting user:', error);
showErrorMessage('Failed to delete user: ' + error.message);
}
}
}, 'Are you sure you want to delete this user? This action cannot be undone.');
}
// Handle create user form submission
@@ -1102,7 +1102,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
// Delete access key
async function deleteAccessKey(username, accessKey) {
if (confirm('Are you sure you want to delete this access key?')) {
showDeleteConfirm(accessKey, async function() {
try {
const response = await fetch(`/api/users/${username}/access-keys/${accessKey}`, {
method: 'DELETE'
@@ -1121,7 +1121,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
console.error('Error deleting access key:', error);
showErrorMessage('Failed to delete access key: ' + error.message);
}
}
}, 'Are you sure you want to delete this access key?');
}
@@ -193,7 +193,7 @@ func ObjectStoreUsers(data dash.ObjectStoreUsersData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</small></div></div></div><!-- Create User Modal --><div class=\"modal fade\" id=\"createUserModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-user-plus me-2\"></i>Create New User</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\"><form id=\"createUserForm\"><div class=\"mb-3\"><label for=\"username\" class=\"form-label\">Username *</label> <input type=\"text\" class=\"form-control\" id=\"username\" name=\"username\" required></div><div class=\"mb-3\"><label for=\"email\" class=\"form-label\">Email</label> <input type=\"email\" class=\"form-control\" id=\"email\" name=\"email\"></div><div class=\"mb-3\"><label for=\"actions\" class=\"form-label\">Permissions</label> <select multiple class=\"form-control\" id=\"actions\" name=\"actions\" size=\"10\"><option value=\"Admin\">Admin (Full Access)</option> <option value=\"Read\">Read</option> <option value=\"Write\">Write</option> <option value=\"List\">List</option> <option value=\"Tagging\">Tagging</option> <optgroup label=\"Object Lock Permissions\"><option value=\"BypassGovernanceRetention\">Bypass Governance Retention</option> <option value=\"GetObjectRetention\">Get Object Retention</option> <option value=\"PutObjectRetention\">Put Object Retention</option> <option value=\"GetObjectLegalHold\">Get Object Legal Hold</option> <option value=\"PutObjectLegalHold\">Put Object Legal Hold</option> <option value=\"GetBucketObjectLockConfiguration\">Get Bucket Object Lock Configuration</option> <option value=\"PutBucketObjectLockConfiguration\">Put Bucket Object Lock Configuration</option></optgroup></select> <small class=\"form-text text-muted\">Hold Ctrl/Cmd to select multiple permissions</small></div><div class=\"mb-3\"><label class=\"form-label\">Bucket Scope</label> <small class=\"form-text text-muted d-block mb-2\">Apply selected permissions to specific buckets or all buckets</small><div class=\"form-check mb-2\"><input class=\"form-check-input\" type=\"radio\" name=\"bucketScope\" id=\"allBuckets\" value=\"all\" checked onchange=\"toggleBucketList()\"> <label class=\"form-check-label\" for=\"allBuckets\">All Buckets</label></div><div class=\"form-check mb-2\"><input class=\"form-check-input\" type=\"radio\" name=\"bucketScope\" id=\"specificBuckets\" value=\"specific\" onchange=\"toggleBucketList()\"> <label class=\"form-check-label\" for=\"specificBuckets\">Specific Buckets</label></div><div id=\"bucketSelectionList\" class=\"mt-2\" style=\"display: none;\"><select multiple class=\"form-select\" id=\"selectedBuckets\" size=\"5\"><!-- Options loaded dynamically --></select> <small class=\"form-text text-muted\">Hold Ctrl/Cmd to select multiple buckets</small></div></div><div class=\"mb-3\"><label for=\"policies\" class=\"form-label\">Attached Policies</label> <select multiple class=\"form-control\" id=\"policies\" name=\"policies\" size=\"5\"><!-- Options loaded dynamically --></select> <small class=\"form-text text-muted\">Hold Ctrl/Cmd to select multiple policies</small></div><div class=\"mb-3 form-check\"><input type=\"checkbox\" class=\"form-check-input\" id=\"generateKey\" name=\"generateKey\" checked> <label class=\"form-check-label\" for=\"generateKey\">Generate access key automatically</label></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"handleCreateUser()\">Create User</button></div></div></div></div><!-- Edit User Modal --><div class=\"modal fade\" id=\"editUserModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-user-edit me-2\"></i>Edit User</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\"><form id=\"editUserForm\"><input type=\"hidden\" id=\"editUsername\" name=\"username\"><div class=\"mb-3\"><label for=\"editEmail\" class=\"form-label\">Email</label> <input type=\"email\" class=\"form-control\" id=\"editEmail\" name=\"email\"></div><div class=\"mb-3\"><label for=\"editActions\" class=\"form-label\">Permissions</label> <select multiple class=\"form-control\" id=\"editActions\" name=\"actions\" size=\"10\"><option value=\"Admin\">Admin (Full Access)</option> <option value=\"Read\">Read</option> <option value=\"Write\">Write</option> <option value=\"List\">List</option> <option value=\"Tagging\">Tagging</option> <optgroup label=\"Object Lock Permissions\"><option value=\"BypassGovernanceRetention\">Bypass Governance Retention</option> <option value=\"GetObjectRetention\">Get Object Retention</option> <option value=\"PutObjectRLine truncated
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</small></div></div></div><!-- Create User Modal --><div class=\"modal fade\" id=\"createUserModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-user-plus me-2\"></i>Create New User</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\"><form id=\"createUserForm\"><div class=\"mb-3\"><label for=\"username\" class=\"form-label\">Username *</label> <input type=\"text\" class=\"form-control\" id=\"username\" name=\"username\" required></div><div class=\"mb-3\"><label for=\"email\" class=\"form-label\">Email</label> <input type=\"email\" class=\"form-control\" id=\"email\" name=\"email\"></div><div class=\"mb-3\"><label for=\"actions\" class=\"form-label\">Permissions</label> <select multiple class=\"form-control\" id=\"actions\" name=\"actions\" size=\"10\"><option value=\"Admin\">Admin (Full Access)</option> <option value=\"Read\">Read</option> <option value=\"Write\">Write</option> <option value=\"List\">List</option> <option value=\"Tagging\">Tagging</option> <optgroup label=\"Object Lock Permissions\"><option value=\"BypassGovernanceRetention\">Bypass Governance Retention</option> <option value=\"GetObjectRetention\">Get Object Retention</option> <option value=\"PutObjectRetention\">Put Object Retention</option> <option value=\"GetObjectLegalHold\">Get Object Legal Hold</option> <option value=\"PutObjectLegalHold\">Put Object Legal Hold</option> <option value=\"GetBucketObjectLockConfiguration\">Get Bucket Object Lock Configuration</option> <option value=\"PutBucketObjectLockConfiguration\">Put Bucket Object Lock Configuration</option></optgroup></select> <small class=\"form-text text-muted\">Hold Ctrl/Cmd to select multiple permissions</small></div><div class=\"mb-3\"><label class=\"form-label\">Bucket Scope</label> <small class=\"form-text text-muted d-block mb-2\">Apply selected permissions to specific buckets or all buckets</small><div class=\"form-check mb-2\"><input class=\"form-check-input\" type=\"radio\" name=\"bucketScope\" id=\"allBuckets\" value=\"all\" checked onchange=\"toggleBucketList()\"> <label class=\"form-check-label\" for=\"allBuckets\">All Buckets</label></div><div class=\"form-check mb-2\"><input class=\"form-check-input\" type=\"radio\" name=\"bucketScope\" id=\"specificBuckets\" value=\"specific\" onchange=\"toggleBucketList()\"> <label class=\"form-check-label\" for=\"specificBuckets\">Specific Buckets</label></div><div id=\"bucketSelectionList\" class=\"mt-2\" style=\"display: none;\"><select multiple class=\"form-select\" id=\"selectedBuckets\" size=\"5\"><!-- Options loaded dynamically --></select> <small class=\"form-text text-muted\">Hold Ctrl/Cmd to select multiple buckets</small></div></div><div class=\"mb-3\"><label for=\"policies\" class=\"form-label\">Attached Policies</label> <select multiple class=\"form-control\" id=\"policies\" name=\"policies\" size=\"5\"><!-- Options loaded dynamically --></select> <small class=\"form-text text-muted\">Hold Ctrl/Cmd to select multiple policies</small></div><div class=\"mb-3 form-check\"><input type=\"checkbox\" class=\"form-check-input\" id=\"generateKey\" name=\"generateKey\" checked> <label class=\"form-check-label\" for=\"generateKey\">Generate access key automatically</label></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"handleCreateUser()\">Create User</button></div></div></div></div><!-- Edit User Modal --><div class=\"modal fade\" id=\"editUserModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-user-edit me-2\"></i>Edit User</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\"><form id=\"editUserForm\"><input type=\"hidden\" id=\"editUsername\" name=\"username\"><div class=\"mb-3\"><label for=\"editEmail\" class=\"form-label\">Email</label> <input type=\"email\" class=\"form-control\" id=\"editEmail\" name=\"email\"></div><div class=\"mb-3\"><label for=\"editActions\" class=\"form-label\">Permissions</label> <select multiple class=\"form-control\" id=\"editActions\" name=\"actions\" size=\"10\"><option value=\"Admin\">Admin (Full Access)</option> <option value=\"Read\">Read</option> <option value=\"Write\">Write</option> <option value=\"List\">List</option> <option value=\"Tagging\">Tagging</option> <optgroup label=\"Object Lock Permissions\"><option value=\"BypassGovernanceRetention\">Bypass Governance Retention</option> <option value=\"GetObjectRetention\">Get Object Retention</option> <option value=\"PutObjectRLine truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+3 -3
View File
@@ -635,7 +635,7 @@ templ Policies(data dash.PoliciesData) {
}
function deletePolicy(policyName) {
if (confirm('Are you sure you want to delete policy "' + policyName + '"?')) {
showDeleteConfirm(policyName, function() {
fetch('/api/object-store/policies/' + encodeURIComponent(policyName), {
method: 'DELETE'
})
@@ -643,7 +643,7 @@ templ Policies(data dash.PoliciesData) {
.then(data => {
if (data.success) {
alert('Policy deleted successfully!');
location.reload(); // Refresh the page
location.reload();
} else {
alert('Error deleting policy: ' + (data.error || 'Unknown error'));
}
@@ -652,7 +652,7 @@ templ Policies(data dash.PoliciesData) {
console.error('Error:', error);
alert('Error deleting policy: ' + error.message);
});
}
}, 'Are you sure you want to delete this policy?');
}
</script>
}
+1 -1
View File
@@ -193,7 +193,7 @@ func Policies(data dash.PoliciesData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</tbody></table></div></div></div></div></div></div><!-- Create Policy Modal --><div class=\"modal fade\" id=\"createPolicyModal\" tabindex=\"-1\" aria-labelledby=\"createPolicyModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog modal-lg\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"createPolicyModalLabel\"><i class=\"fas fa-shield-alt me-2\"></i>Create IAM Policy</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><form id=\"createPolicyForm\"><div class=\"mb-3\"><label for=\"policyName\" class=\"form-label\">Policy Name</label> <input type=\"text\" class=\"form-control\" id=\"policyName\" name=\"name\" required placeholder=\"e.g., S3ReadOnlyPolicy\"><div class=\"form-text\">Enter a unique name for this policy (alphanumeric and underscores only)</div></div><div class=\"mb-3\"><label for=\"policyDocument\" class=\"form-label\">Policy Document</label> <textarea class=\"form-control\" id=\"policyDocument\" name=\"document\" rows=\"15\" required placeholder=\"Enter IAM policy JSON document...\"></textarea><div class=\"form-text\">Enter the policy document in AWS IAM JSON format</div></div><div class=\"mb-3\"><div class=\"row\"><div class=\"col-md-6\"><button type=\"button\" class=\"btn btn-outline-info btn-sm\" onclick=\"insertSamplePolicy()\"><i class=\"fas fa-file-alt me-1\"></i>Use Sample Policy</button></div><div class=\"col-md-6 text-end\"><button type=\"button\" class=\"btn btn-outline-secondary btn-sm\" onclick=\"validatePolicyDocument()\"><i class=\"fas fa-check me-1\"></i>Validate JSON</button></div></div></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"createPolicy()\"><i class=\"fas fa-plus me-1\"></i>Create Policy</button></div></div></div></div><!-- View Policy Modal --><div class=\"modal fade\" id=\"viewPolicyModal\" tabindex=\"-1\" aria-labelledby=\"viewPolicyModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog modal-lg\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"viewPolicyModalLabel\"><i class=\"fas fa-eye me-2\"></i>View IAM Policy</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><div id=\"viewPolicyContent\"><div class=\"text-center\"><div class=\"spinner-border\" role=\"status\"><span class=\"visually-hidden\">Loading...</span></div><p class=\"mt-2\">Loading policy...</p></div></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Close</button> <button type=\"button\" class=\"btn btn-primary\" id=\"editFromViewBtn\"><i class=\"fas fa-edit me-1\"></i>Edit Policy</button></div></div></div></div><!-- Edit Policy Modal --><div class=\"modal fade\" id=\"editPolicyModal\" tabindex=\"-1\" aria-labelledby=\"editPolicyModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog modal-lg\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"editPolicyModalLabel\"><i class=\"fas fa-edit me-2\"></i>Edit IAM Policy</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><form id=\"editPolicyForm\"><div class=\"mb-3\"><label for=\"editPolicyName\" class=\"form-label\">Policy Name</label> <input type=\"text\" class=\"form-control\" id=\"editPolicyName\" name=\"name\" readonly><div class=\"form-text\">Policy name cannot be changed</div></div><div class=\"mb-3\"><label for=\"editPolicyDocument\" class=\"form-label\">Policy Document</label> <textarea class=\"form-control\" id=\"editPolicyDocument\" name=\"document\" rows=\"15\" required></textarea><div class=\"form-text\">Edit the policy document in AWS IAM JSON format</div></div><div class=\"mb-3\"><div class=\"row\"><div class=\"col-md-6\"><button type=\"button\" class=\"btn btn-outline-info btn-sm\" onclick=\"insertSamplePolicyEdit()\"><i class=\"fas fa-file-alt me-1\"></i>Reset to Sample</button></div><div class=\"col-md-6 text-end\"><button type=\"button\" class=\"btn btn-outline-secondary btn-sm\" onclick=\"validateEditPolicyDocument()\"><i class=\"fas fa-check me-1\"></i>Validate JSON</button></div></div></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"updatePolicy()\"><i class=\"fas fa-save me-1\"></i>Save Changes</button></div></div></div></div><!-- JavaScript for Policy Management --><script>\n // Current policy being viewed/edited\n let currentPolicy = null;\n \n // Event listeners for policy actions\n document.adLine truncated
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</tbody></table></div></div></div></div></div></div><!-- Create Policy Modal --><div class=\"modal fade\" id=\"createPolicyModal\" tabindex=\"-1\" aria-labelledby=\"createPolicyModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog modal-lg\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"createPolicyModalLabel\"><i class=\"fas fa-shield-alt me-2\"></i>Create IAM Policy</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><form id=\"createPolicyForm\"><div class=\"mb-3\"><label for=\"policyName\" class=\"form-label\">Policy Name</label> <input type=\"text\" class=\"form-control\" id=\"policyName\" name=\"name\" required placeholder=\"e.g., S3ReadOnlyPolicy\"><div class=\"form-text\">Enter a unique name for this policy (alphanumeric and underscores only)</div></div><div class=\"mb-3\"><label for=\"policyDocument\" class=\"form-label\">Policy Document</label> <textarea class=\"form-control\" id=\"policyDocument\" name=\"document\" rows=\"15\" required placeholder=\"Enter IAM policy JSON document...\"></textarea><div class=\"form-text\">Enter the policy document in AWS IAM JSON format</div></div><div class=\"mb-3\"><div class=\"row\"><div class=\"col-md-6\"><button type=\"button\" class=\"btn btn-outline-info btn-sm\" onclick=\"insertSamplePolicy()\"><i class=\"fas fa-file-alt me-1\"></i>Use Sample Policy</button></div><div class=\"col-md-6 text-end\"><button type=\"button\" class=\"btn btn-outline-secondary btn-sm\" onclick=\"validatePolicyDocument()\"><i class=\"fas fa-check me-1\"></i>Validate JSON</button></div></div></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"createPolicy()\"><i class=\"fas fa-plus me-1\"></i>Create Policy</button></div></div></div></div><!-- View Policy Modal --><div class=\"modal fade\" id=\"viewPolicyModal\" tabindex=\"-1\" aria-labelledby=\"viewPolicyModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog modal-lg\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"viewPolicyModalLabel\"><i class=\"fas fa-eye me-2\"></i>View IAM Policy</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><div id=\"viewPolicyContent\"><div class=\"text-center\"><div class=\"spinner-border\" role=\"status\"><span class=\"visually-hidden\">Loading...</span></div><p class=\"mt-2\">Loading policy...</p></div></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Close</button> <button type=\"button\" class=\"btn btn-primary\" id=\"editFromViewBtn\"><i class=\"fas fa-edit me-1\"></i>Edit Policy</button></div></div></div></div><!-- Edit Policy Modal --><div class=\"modal fade\" id=\"editPolicyModal\" tabindex=\"-1\" aria-labelledby=\"editPolicyModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog modal-lg\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"editPolicyModalLabel\"><i class=\"fas fa-edit me-2\"></i>Edit IAM Policy</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><form id=\"editPolicyForm\"><div class=\"mb-3\"><label for=\"editPolicyName\" class=\"form-label\">Policy Name</label> <input type=\"text\" class=\"form-control\" id=\"editPolicyName\" name=\"name\" readonly><div class=\"form-text\">Policy name cannot be changed</div></div><div class=\"mb-3\"><label for=\"editPolicyDocument\" class=\"form-label\">Policy Document</label> <textarea class=\"form-control\" id=\"editPolicyDocument\" name=\"document\" rows=\"15\" required></textarea><div class=\"form-text\">Edit the policy document in AWS IAM JSON format</div></div><div class=\"mb-3\"><div class=\"row\"><div class=\"col-md-6\"><button type=\"button\" class=\"btn btn-outline-info btn-sm\" onclick=\"insertSamplePolicyEdit()\"><i class=\"fas fa-file-alt me-1\"></i>Reset to Sample</button></div><div class=\"col-md-6 text-end\"><button type=\"button\" class=\"btn btn-outline-secondary btn-sm\" onclick=\"validateEditPolicyDocument()\"><i class=\"fas fa-check me-1\"></i>Validate JSON</button></div></div></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"updatePolicy()\"><i class=\"fas fa-save me-1\"></i>Save Changes</button></div></div></div></div><!-- JavaScript for Policy Management --><script>\n // Current policy being viewed/edited\n let currentPolicy = null;\n \n // Event listeners for policy actions\n document.adLine truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+2 -2
View File
@@ -373,7 +373,7 @@ templ ServiceAccounts(data dash.ServiceAccountsData) {
}
async function deleteSA(id) {
if (confirm('Are you sure you want to delete this service account? This action cannot be undone.')) {
showDeleteConfirm(id, async function() {
try {
const response = await fetch(`/api/service-accounts/${id}`, {
method: 'DELETE'
@@ -390,7 +390,7 @@ templ ServiceAccounts(data dash.ServiceAccountsData) {
console.error('Error deleting service account:', error);
showErrorMessage('Failed to delete: ' + error.message);
}
}
}, 'Are you sure you want to delete this service account? This action cannot be undone.');
}
async function handleCreateServiceAccount() {
@@ -272,7 +272,7 @@ func ServiceAccounts(data dash.ServiceAccountsData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</select> <small class=\"form-text text-muted\">The service account will inherit permissions from this user</small></div><div class=\"mb-3\"><label for=\"description\" class=\"form-label\">Description</label> <textarea class=\"form-control\" id=\"description\" name=\"description\" rows=\"2\" placeholder=\"What is this service account used for?\"></textarea></div><div class=\"mb-3\"><label for=\"expiration\" class=\"form-label\">Expiration (optional)</label> <input type=\"datetime-local\" class=\"form-control\" id=\"expiration\" name=\"expiration\"> <small class=\"form-text text-muted\">Leave empty for no expiration</small></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"handleCreateServiceAccount()\">Create</button></div></div></div></div><!-- Service Account Details Modal --><div class=\"modal fade\" id=\"saDetailsModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog modal-lg\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-robot me-2\"></i>Service Account Details</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\" id=\"saDetailsContent\"><!-- Content will be loaded dynamically --></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Close</button></div></div></div></div><!-- Credentials Display Modal --><div class=\"modal fade\" id=\"credentialsModal\" tabindex=\"-1\" role=\"dialog\" data-bs-backdrop=\"static\" data-bs-keyboard=\"false\"><div class=\"modal-dialog modal-lg\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header bg-success text-white\"><h5 class=\"modal-title\"><i class=\"fas fa-check-circle me-2\"></i>Service Account Created Successfully</h5></div><div class=\"modal-body\"><div class=\"alert alert-warning\"><i class=\"fas fa-exclamation-triangle me-2\"></i> <strong>Important:</strong> This is the only time you will see the secret access key. Please save it securely.</div><div class=\"mb-4\"><h6 class=\"text-muted mb-3\">AWS CLI Configuration</h6><p class=\"text-muted small\">Use these credentials to configure AWS CLI or SDKs:</p><div class=\"mb-3\"><label class=\"form-label fw-bold\">AWS_ACCESS_KEY_ID</label><div class=\"input-group\"><input type=\"text\" class=\"form-control font-monospace\" id=\"displayAccessKey\" readonly> <button class=\"btn btn-outline-secondary\" type=\"button\" onclick=\"copyCredentialToClipboard(this, 'displayAccessKey')\"><i class=\"fas fa-copy\"></i> Copy</button></div></div><div class=\"mb-3\"><label class=\"form-label fw-bold\">AWS_SECRET_ACCESS_KEY</label><div class=\"input-group\"><input type=\"text\" class=\"form-control font-monospace\" id=\"displaySecretKey\" readonly> <button class=\"btn btn-outline-secondary\" type=\"button\" onclick=\"copyCredentialToClipboard(this, 'displaySecretKey')\"><i class=\"fas fa-copy\"></i> Copy</button></div></div></div><div class=\"bg-light p-3 rounded\"><h6 class=\"text-muted mb-2\">Example AWS CLI Usage:</h6><div class=\"font-monospace small\"><div>export AWS_ACCESS_KEY_ID=<span id=\"exampleAccessKey\"></span></div><div>export AWS_SECRET_ACCESS_KEY=<span id=\"exampleSecretKey\"></span></div><div>export AWS_ENDPOINT_URL=http://localhost:8333</div><div class=\"mt-2\"># List buckets</div><div>aws s3 ls</div><div class=\"mt-2\"># Upload a file</div><div>aws s3 cp myfile.txt s3://mybucket/</div></div></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-primary\" onclick=\"closeCredentialsModal()\"><i class=\"fas fa-check me-1\"></i>I have saved the credentials</button></div></div></div></div><!-- JavaScript for service account management --><script>\n document.addEventListener('DOMContentLoaded', function() {\n document.addEventListener('click', function(e) {\n const button = e.target.closest('[data-action]');\n if (!button) return;\n \n const action = button.getAttribute('data-action');\n const saId = button.getAttribute('data-sa-id');\n \n switch (action) {\n case 'show-sa-details':\n showSADetails(saId);\n break;\n case 'toggle-sa-status':\n toggleSAStatus(saId, button.getAttribute('data-current-status'));\n break;\n case 'delete-sa':\n deleteSA(saId);\n break;\n }\n });\n });\n\n async function showSADetails(id) {\n try {\n const response = await fetch(`/api/service-accounts/$Line truncated
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</select> <small class=\"form-text text-muted\">The service account will inherit permissions from this user</small></div><div class=\"mb-3\"><label for=\"description\" class=\"form-label\">Description</label> <textarea class=\"form-control\" id=\"description\" name=\"description\" rows=\"2\" placeholder=\"What is this service account used for?\"></textarea></div><div class=\"mb-3\"><label for=\"expiration\" class=\"form-label\">Expiration (optional)</label> <input type=\"datetime-local\" class=\"form-control\" id=\"expiration\" name=\"expiration\"> <small class=\"form-text text-muted\">Leave empty for no expiration</small></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"handleCreateServiceAccount()\">Create</button></div></div></div></div><!-- Service Account Details Modal --><div class=\"modal fade\" id=\"saDetailsModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog modal-lg\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-robot me-2\"></i>Service Account Details</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\" id=\"saDetailsContent\"><!-- Content will be loaded dynamically --></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Close</button></div></div></div></div><!-- Credentials Display Modal --><div class=\"modal fade\" id=\"credentialsModal\" tabindex=\"-1\" role=\"dialog\" data-bs-backdrop=\"static\" data-bs-keyboard=\"false\"><div class=\"modal-dialog modal-lg\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header bg-success text-white\"><h5 class=\"modal-title\"><i class=\"fas fa-check-circle me-2\"></i>Service Account Created Successfully</h5></div><div class=\"modal-body\"><div class=\"alert alert-warning\"><i class=\"fas fa-exclamation-triangle me-2\"></i> <strong>Important:</strong> This is the only time you will see the secret access key. Please save it securely.</div><div class=\"mb-4\"><h6 class=\"text-muted mb-3\">AWS CLI Configuration</h6><p class=\"text-muted small\">Use these credentials to configure AWS CLI or SDKs:</p><div class=\"mb-3\"><label class=\"form-label fw-bold\">AWS_ACCESS_KEY_ID</label><div class=\"input-group\"><input type=\"text\" class=\"form-control font-monospace\" id=\"displayAccessKey\" readonly> <button class=\"btn btn-outline-secondary\" type=\"button\" onclick=\"copyCredentialToClipboard(this, 'displayAccessKey')\"><i class=\"fas fa-copy\"></i> Copy</button></div></div><div class=\"mb-3\"><label class=\"form-label fw-bold\">AWS_SECRET_ACCESS_KEY</label><div class=\"input-group\"><input type=\"text\" class=\"form-control font-monospace\" id=\"displaySecretKey\" readonly> <button class=\"btn btn-outline-secondary\" type=\"button\" onclick=\"copyCredentialToClipboard(this, 'displaySecretKey')\"><i class=\"fas fa-copy\"></i> Copy</button></div></div></div><div class=\"bg-light p-3 rounded\"><h6 class=\"text-muted mb-2\">Example AWS CLI Usage:</h6><div class=\"font-monospace small\"><div>export AWS_ACCESS_KEY_ID=<span id=\"exampleAccessKey\"></span></div><div>export AWS_SECRET_ACCESS_KEY=<span id=\"exampleSecretKey\"></span></div><div>export AWS_ENDPOINT_URL=http://localhost:8333</div><div class=\"mt-2\"># List buckets</div><div>aws s3 ls</div><div class=\"mt-2\"># Upload a file</div><div>aws s3 cp myfile.txt s3://mybucket/</div></div></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-primary\" onclick=\"closeCredentialsModal()\"><i class=\"fas fa-check me-1\"></i>I have saved the credentials</button></div></div></div></div><!-- JavaScript for service account management --><script>\n document.addEventListener('DOMContentLoaded', function() {\n document.addEventListener('click', function(e) {\n const button = e.target.closest('[data-action]');\n if (!button) return;\n \n const action = button.getAttribute('data-action');\n const saId = button.getAttribute('data-sa-id');\n \n switch (action) {\n case 'show-sa-details':\n showSADetails(saId);\n break;\n case 'toggle-sa-status':\n toggleSAStatus(saId, button.getAttribute('data-current-status'));\n break;\n case 'delete-sa':\n deleteSA(saId);\n break;\n }\n });\n });\n\n async function showSADetails(id) {\n try {\n const response = await fetch(`/api/service-accounts/$Line truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+2 -2
View File
@@ -104,13 +104,13 @@ templ TaskConfig(data *maintenance.TaskConfigData) {
<script>
function resetForm() {
if (confirm('Are you sure you want to reset all settings to their default values?')) {
showConfirm('Are you sure you want to reset all settings to their default values?', function() {
// Find all form inputs and reset them
const form = document.querySelector('form');
if (form) {
form.reset();
}
}
});
}
// Auto-save form data to localStorage for recovery
+2 -2
View File
@@ -127,7 +127,7 @@ templ TaskConfigSchema(data *maintenance.TaskConfigData, schema *tasks.TaskConfi
<script>
function resetToDefaults() {
if (confirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.')) {
showConfirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.', function() {
// Reset form fields to their default values
const form = document.getElementById('taskConfigForm');
const schemaFields = window.taskConfigSchema ? window.taskConfigSchema.fields : {};
@@ -154,7 +154,7 @@ templ TaskConfigSchema(data *maintenance.TaskConfigData, schema *tasks.TaskConfi
}
}
});
}
});
}
function convertSecondsToTaskIntervalValueUnit(totalSeconds) {
@@ -170,7 +170,7 @@ func TaskConfigSchema(data *maintenance.TaskConfigData, schema *tasks.TaskConfig
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></div></div></div></div></div><script>\n function resetToDefaults() {\n if (confirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.')) {\n // Reset form fields to their default values\n const form = document.getElementById('taskConfigForm');\n const schemaFields = window.taskConfigSchema ? window.taskConfigSchema.fields : {};\n \n Object.keys(schemaFields).forEach(fieldName => {\n const field = schemaFields[fieldName];\n const element = document.getElementById(fieldName);\n \n if (element && field.default_value !== undefined) {\n if (field.input_type === 'checkbox') {\n element.checked = field.default_value;\n } else if (field.input_type === 'interval') {\n // Handle interval fields with value and unit\n const valueElement = document.getElementById(fieldName + '_value');\n const unitElement = document.getElementById(fieldName + '_unit');\n if (valueElement && unitElement && field.default_value) {\n const defaultSeconds = field.default_value;\n const { value, unit } = convertSecondsToTaskIntervalValueUnit(defaultSeconds);\n valueElement.value = value;\n unitElement.value = unit;\n }\n } else {\n element.value = field.default_value;\n }\n }\n });\n }\n }\n\n function convertSecondsToTaskIntervalValueUnit(totalSeconds) {\n if (totalSeconds === 0) {\n return { value: 0, unit: 'minutes' };\n }\n\n // Check if it's evenly divisible by days\n if (totalSeconds % (24 * 3600) === 0) {\n return { value: totalSeconds / (24 * 3600), unit: 'days' };\n }\n\n // Check if it's evenly divisible by hours\n if (totalSeconds % 3600 === 0) {\n return { value: totalSeconds / 3600, unit: 'hours' };\n }\n\n // Default to minutes\n return { value: totalSeconds / 60, unit: 'minutes' };\n }\n\n // Store schema data for JavaScript access (moved to after div is created)\n </script><!-- Hidden element to store schema data --><div data-task-schema=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></div></div></div></div></div><script>\n function resetToDefaults() {\n showConfirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.', function() {\n // Reset form fields to their default values\n const form = document.getElementById('taskConfigForm');\n const schemaFields = window.taskConfigSchema ? window.taskConfigSchema.fields : {};\n \n Object.keys(schemaFields).forEach(fieldName => {\n const field = schemaFields[fieldName];\n const element = document.getElementById(fieldName);\n \n if (element && field.default_value !== undefined) {\n if (field.input_type === 'checkbox') {\n element.checked = field.default_value;\n } else if (field.input_type === 'interval') {\n // Handle interval fields with value and unit\n const valueElement = document.getElementById(fieldName + '_value');\n const unitElement = document.getElementById(fieldName + '_unit');\n if (valueElement && unitElement && field.default_value) {\n const defaultSeconds = field.default_value;\n const { value, unit } = convertSecondsToTaskIntervalValueUnit(defaultSeconds);\n valueElement.value = value;\n unitElement.value = unit;\n }\n } else {\n element.value = field.default_value;\n }\n }\n });\n });\n }\n\n function convertSecondsToTaskIntervalValueUnit(totalSeconds) {\n if (totalSeconds === 0) {\n return { value: 0, unit: 'minutes' };\n }\n\n // Check if it's evenly divisible by days\n if (totalSeconds % (24 * 3600) === 0) {\n return { value: totalSeconds / (24 * 3600), unit: 'days' };\n }\n\n // Check if it's evenly divisible by hours\n if (totalSeconds % 3600 === 0) {\n return { value: totalSeconds / 3600, unit: 'hours' };\n }\n\n // Default to minutes\n return { value: totalSeconds / 60, unit: 'minutes' };\n }\n\n // Store schema data for JavaScript access (moved to after div is created)\n </script><!-- Hidden element to store schema data --><div data-task-schema=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+1 -1
View File
@@ -163,7 +163,7 @@ func TaskConfig(data *maintenance.TaskConfigData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p></div></div></div></div></div></div></div><script>\n function resetForm() {\n if (confirm('Are you sure you want to reset all settings to their default values?')) {\n // Find all form inputs and reset them\n const form = document.querySelector('form');\n if (form) {\n form.reset();\n }\n }\n }\n\n // Auto-save form data to localStorage for recovery\n document.addEventListener('DOMContentLoaded', function() {\n const form = document.querySelector('form');\n if (form) {\n const taskType = '{string(data.TaskType)}';\n const storageKey = 'taskConfig_' + taskType;\n\n // Load saved data\n const savedData = localStorage.getItem(storageKey);\n if (savedData) {\n try {\n const data = JSON.parse(savedData);\n Object.keys(data).forEach(key => {\n const input = form.querySelector(`[name=\"${key}\"]`);\n if (input) {\n if (input.type === 'checkbox') {\n input.checked = data[key];\n } else {\n input.value = data[key];\n }\n }\n });\n } catch (e) {\n console.warn('Failed to load saved configuration:', e);\n }\n }\n\n // Save data on input change\n form.addEventListener('input', function() {\n const formData = new FormData(form);\n const data = {};\n for (let [key, value] of formData.entries()) {\n data[key] = value;\n }\n localStorage.setItem(storageKey, JSON.stringify(data));\n });\n\n // Clear saved data on successful submit\n form.addEventListener('submit', function() {\n localStorage.removeItem(storageKey);\n });\n }\n });\n </script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p></div></div></div></div></div></div></div><script>\n function resetForm() {\n showConfirm('Are you sure you want to reset all settings to their default values?', function() {\n // Find all form inputs and reset them\n const form = document.querySelector('form');\n if (form) {\n form.reset();\n }\n });\n }\n\n // Auto-save form data to localStorage for recovery\n document.addEventListener('DOMContentLoaded', function() {\n const form = document.querySelector('form');\n if (form) {\n const taskType = '{string(data.TaskType)}';\n const storageKey = 'taskConfig_' + taskType;\n\n // Load saved data\n const savedData = localStorage.getItem(storageKey);\n if (savedData) {\n try {\n const data = JSON.parse(savedData);\n Object.keys(data).forEach(key => {\n const input = form.querySelector(`[name=\"${key}\"]`);\n if (input) {\n if (input.type === 'checkbox') {\n input.checked = data[key];\n } else {\n input.value = data[key];\n }\n }\n });\n } catch (e) {\n console.warn('Failed to load saved configuration:', e);\n }\n }\n\n // Save data on input change\n form.addEventListener('input', function() {\n const formData = new FormData(form);\n const data = {};\n for (let [key, value] of formData.entries()) {\n data[key] = value;\n }\n localStorage.setItem(storageKey, JSON.stringify(data));\n });\n\n // Clear saved data on successful submit\n form.addEventListener('submit', function() {\n localStorage.removeItem(storageKey);\n });\n }\n });\n </script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+2 -2
View File
@@ -121,9 +121,9 @@ templ TaskConfigTempl(data *TaskConfigTemplData) {
// Reset form function
function resetForm() {
if (confirm('Are you sure you want to reset all changes?')) {
showConfirm('Are you sure you want to reset all changes?', function() {
location.reload();
}
});
}
// Test configuration function
@@ -101,7 +101,7 @@ func TaskConfigTempl(data *TaskConfigTemplData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<!-- Form actions --><div class=\"row\"><div class=\"col-12\"><div class=\"card\"><div class=\"card-body\"><div class=\"d-flex justify-content-between\"><div><button type=\"submit\" class=\"btn btn-primary\"><i class=\"fas fa-save me-1\"></i> Save Configuration</button> <button type=\"button\" class=\"btn btn-outline-secondary ms-2\" onclick=\"resetForm()\"><i class=\"fas fa-undo me-1\"></i> Reset</button></div><div><button type=\"button\" class=\"btn btn-outline-info\" onclick=\"testConfiguration()\"><i class=\"fas fa-play me-1\"></i> Test Configuration</button></div></div></div></div></div></div></form></div><script>\n // Form validation\n (function() {\n 'use strict';\n window.addEventListener('load', function() {\n var forms = document.getElementsByClassName('needs-validation');\n var validation = Array.prototype.filter.call(forms, function(form) {\n form.addEventListener('submit', function(event) {\n if (form.checkValidity() === false) {\n event.preventDefault();\n event.stopPropagation();\n }\n form.classList.add('was-validated');\n }, false);\n });\n }, false);\n })();\n\n // Auto-save functionality\n let autoSaveTimeout;\n function autoSave() {\n clearTimeout(autoSaveTimeout);\n autoSaveTimeout = setTimeout(function() {\n const formData = new FormData(document.querySelector('form'));\n localStorage.setItem('task_config_' + '{data.TaskType}', JSON.stringify(Object.fromEntries(formData)));\n }, 1000);\n }\n\n // Add auto-save listeners to all form inputs\n document.addEventListener('DOMContentLoaded', function() {\n const form = document.querySelector('form');\n if (form) {\n form.addEventListener('input', autoSave);\n form.addEventListener('change', autoSave);\n }\n });\n\n // Reset form function\n function resetForm() {\n if (confirm('Are you sure you want to reset all changes?')) {\n location.reload();\n }\n }\n\n // Test configuration function\n function testConfiguration() {\n const formData = new FormData(document.querySelector('form'));\n \n // Show loading state\n const testBtn = document.querySelector('button[onclick=\"testConfiguration()\"]');\n const originalContent = testBtn.innerHTML;\n testBtn.innerHTML = '<i class=\"fas fa-spinner fa-spin me-1\"></i>Testing...';\n testBtn.disabled = true;\n \n fetch('/maintenance/config/{data.TaskType}/test', {\n method: 'POST',\n body: formData\n })\n .then(response => response.json())\n .then(data => {\n if (data.success) {\n alert('Configuration test successful!');\n } else {\n alert('Configuration test failed: ' + data.error);\n }\n })\n .catch(error => {\n alert('Test failed: ' + error);\n })\n .finally(() => {\n testBtn.innerHTML = originalContent;\n testBtn.disabled = false;\n });\n }\n </script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<!-- Form actions --><div class=\"row\"><div class=\"col-12\"><div class=\"card\"><div class=\"card-body\"><div class=\"d-flex justify-content-between\"><div><button type=\"submit\" class=\"btn btn-primary\"><i class=\"fas fa-save me-1\"></i> Save Configuration</button> <button type=\"button\" class=\"btn btn-outline-secondary ms-2\" onclick=\"resetForm()\"><i class=\"fas fa-undo me-1\"></i> Reset</button></div><div><button type=\"button\" class=\"btn btn-outline-info\" onclick=\"testConfiguration()\"><i class=\"fas fa-play me-1\"></i> Test Configuration</button></div></div></div></div></div></div></form></div><script>\n // Form validation\n (function() {\n 'use strict';\n window.addEventListener('load', function() {\n var forms = document.getElementsByClassName('needs-validation');\n var validation = Array.prototype.filter.call(forms, function(form) {\n form.addEventListener('submit', function(event) {\n if (form.checkValidity() === false) {\n event.preventDefault();\n event.stopPropagation();\n }\n form.classList.add('was-validated');\n }, false);\n });\n }, false);\n })();\n\n // Auto-save functionality\n let autoSaveTimeout;\n function autoSave() {\n clearTimeout(autoSaveTimeout);\n autoSaveTimeout = setTimeout(function() {\n const formData = new FormData(document.querySelector('form'));\n localStorage.setItem('task_config_' + '{data.TaskType}', JSON.stringify(Object.fromEntries(formData)));\n }, 1000);\n }\n\n // Add auto-save listeners to all form inputs\n document.addEventListener('DOMContentLoaded', function() {\n const form = document.querySelector('form');\n if (form) {\n form.addEventListener('input', autoSave);\n form.addEventListener('change', autoSave);\n }\n });\n\n // Reset form function\n function resetForm() {\n showConfirm('Are you sure you want to reset all changes?', function() {\n location.reload();\n });\n }\n\n // Test configuration function\n function testConfiguration() {\n const formData = new FormData(document.querySelector('form'));\n \n // Show loading state\n const testBtn = document.querySelector('button[onclick=\"testConfiguration()\"]');\n const originalContent = testBtn.innerHTML;\n testBtn.innerHTML = '<i class=\"fas fa-spinner fa-spin me-1\"></i>Testing...';\n testBtn.disabled = true;\n \n fetch('/maintenance/config/{data.TaskType}/test', {\n method: 'POST',\n body: formData\n })\n .then(response => response.json())\n .then(data => {\n if (data.success) {\n alert('Configuration test successful!');\n } else {\n alert('Configuration test failed: ' + data.error);\n }\n })\n .catch(error => {\n alert('Test failed: ' + error);\n })\n .finally(() => {\n testBtn.innerHTML = originalContent;\n testBtn.disabled = false;\n });\n }\n </script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+2 -2
View File
@@ -1056,7 +1056,7 @@ templ TaskDetail(data *maintenance.TaskDetailData) {
}
function cancelTask(taskId) {
if (confirm('Are you sure you want to cancel this task?')) {
showConfirm('Are you sure you want to cancel this task?', function() {
fetch(`/api/maintenance/tasks/${taskId}/cancel`, {
method: 'POST',
headers: {
@@ -1076,7 +1076,7 @@ templ TaskDetail(data *maintenance.TaskDetailData) {
console.error('Error:', error);
alert('Error cancelling task');
});
}
});
}
function refreshTaskLogs(taskId) {
+1 -1
View File
@@ -1617,7 +1617,7 @@ func TaskDetail(data *maintenance.TaskDetailData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "\" onclick=\"exportTaskDetail(this.getAttribute('data-task-id'))\"><i class=\"fas fa-download me-1\"></i> Export Details</button></div></div></div></div></div><!-- Task Logs Modal --><div class=\"modal fade\" id=\"taskLogsModal\" tabindex=\"-1\" aria-labelledby=\"taskLogsModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog modal-xl\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"taskLogsModalLabel\"><i class=\"fas fa-file-text me-2\"></i>Task Logs</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><div id=\"logsLoadingSpinner\" class=\"text-center py-4\" style=\"display: none;\"><div class=\"spinner-border text-primary\" role=\"status\"><span class=\"visually-hidden\">Loading logs...</span></div><p class=\"mt-2\">Fetching logs from worker...</p></div><div id=\"logsError\" class=\"alert alert-danger\" style=\"display: none;\"><i class=\"fas fa-exclamation-triangle me-2\"></i> <span id=\"logsErrorMessage\"></span></div><div id=\"logsContent\" style=\"display: none;\"><div class=\"d-flex justify-content-between align-items-center mb-3\"><div><strong>Task:</strong> <span id=\"logsTaskId\"></span> | <strong>Worker:</strong> <span id=\"logsWorkerId\"></span> | <strong>Entries:</strong> <span id=\"logsCount\"></span></div><div class=\"btn-group\"><button type=\"button\" class=\"btn btn-sm btn-outline-primary\" onclick=\"refreshModalLogs()\"><i class=\"fas fa-sync-alt me-1\"></i>Refresh</button> <button type=\"button\" class=\"btn btn-sm btn-outline-success\" onclick=\"downloadTaskLogs()\"><i class=\"fas fa-download me-1\"></i>Download</button></div></div><div class=\"card\"><div class=\"card-header\"><div class=\"d-flex justify-content-between align-items-center\"><span>Log Entries (Last 100)</span> <small class=\"text-muted\">Newest entries first</small></div></div><div class=\"card-body p-0\"><pre id=\"logsDisplay\" class=\"bg-dark text-light p-3 mb-0\" style=\"max-height: 400px; overflow-y: auto; font-size: 0.85rem; line-height: 1.4;\"></pre></div></div></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Close</button></div></div></div></div><style>\n .timeline-container {\n position: relative;\n padding: 20px 0;\n }\n \n .timeline-progress {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n position: relative;\n max-width: 100%;\n }\n \n .timeline-step {\n display: flex;\n flex-direction: column;\n align-items: center;\n flex: 1;\n position: relative;\n }\n \n .timeline-circle {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n color: white;\n font-weight: bold;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n z-index: 2;\n position: relative;\n }\n \n .timeline-circle.completed {\n background-color: #28a745;\n border: 3px solid #1e7e34;\n }\n \n .timeline-circle.pending {\n background-color: #6c757d;\n border: 3px solid #495057;\n }\n \n .timeline-connector {\n position: absolute;\n top: 20px;\n left: 50%;\n right: -50%;\n height: 4px;\n z-index: 1;\n margin-left: 20px;\n margin-right: 20px;\n }\n \n .timeline-connector.completed {\n background-color: #28a745;\n }\n \n .timeline-connector:not(.completed) {\n background-color: #dee2e6;\n }\n \n .timeline-step:last-child .timeline-connector {\n display: none;\n }\n \n .timeline-label {\n margin-top: 15px;\n text-align: center;\n min-height: 60px;\n }\n \n .timeline-label strong {\n display: block;\n font-size: 0.9rem;\n margin-bottom: 4px;\n }\n \n .timeline-label small {\n font-size: 0.75rem;\n line-height: 1.2;\n }\n \n @media (max-width: 768px) {\n .timeline-progress {\n flex-direction: column;\n align-items: stretch;\n }\n \n .timeline-step {\n flex-direction: row;\n align-items: center;\n margin-bottom: 20px;\n }\n \n .timeline-circle {\n margin-right: 15px;\n flex-shrink: 0;\n }\n \n .timeline-connector {\n display: none;\n }\n \n .timeline-label {\n text-align: left;\n margin-top: 0;\n min-height: auto;\n }\n }\n </style><script>\n // Global variables for current logs modal\n let currentTaskId = '';\n let currentWorkerId = Line truncated
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "\" onclick=\"exportTaskDetail(this.getAttribute('data-task-id'))\"><i class=\"fas fa-download me-1\"></i> Export Details</button></div></div></div></div></div><!-- Task Logs Modal --><div class=\"modal fade\" id=\"taskLogsModal\" tabindex=\"-1\" aria-labelledby=\"taskLogsModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog modal-xl\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"taskLogsModalLabel\"><i class=\"fas fa-file-text me-2\"></i>Task Logs</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><div id=\"logsLoadingSpinner\" class=\"text-center py-4\" style=\"display: none;\"><div class=\"spinner-border text-primary\" role=\"status\"><span class=\"visually-hidden\">Loading logs...</span></div><p class=\"mt-2\">Fetching logs from worker...</p></div><div id=\"logsError\" class=\"alert alert-danger\" style=\"display: none;\"><i class=\"fas fa-exclamation-triangle me-2\"></i> <span id=\"logsErrorMessage\"></span></div><div id=\"logsContent\" style=\"display: none;\"><div class=\"d-flex justify-content-between align-items-center mb-3\"><div><strong>Task:</strong> <span id=\"logsTaskId\"></span> | <strong>Worker:</strong> <span id=\"logsWorkerId\"></span> | <strong>Entries:</strong> <span id=\"logsCount\"></span></div><div class=\"btn-group\"><button type=\"button\" class=\"btn btn-sm btn-outline-primary\" onclick=\"refreshModalLogs()\"><i class=\"fas fa-sync-alt me-1\"></i>Refresh</button> <button type=\"button\" class=\"btn btn-sm btn-outline-success\" onclick=\"downloadTaskLogs()\"><i class=\"fas fa-download me-1\"></i>Download</button></div></div><div class=\"card\"><div class=\"card-header\"><div class=\"d-flex justify-content-between align-items-center\"><span>Log Entries (Last 100)</span> <small class=\"text-muted\">Newest entries first</small></div></div><div class=\"card-body p-0\"><pre id=\"logsDisplay\" class=\"bg-dark text-light p-3 mb-0\" style=\"max-height: 400px; overflow-y: auto; font-size: 0.85rem; line-height: 1.4;\"></pre></div></div></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Close</button></div></div></div></div><style>\n .timeline-container {\n position: relative;\n padding: 20px 0;\n }\n \n .timeline-progress {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n position: relative;\n max-width: 100%;\n }\n \n .timeline-step {\n display: flex;\n flex-direction: column;\n align-items: center;\n flex: 1;\n position: relative;\n }\n \n .timeline-circle {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n color: white;\n font-weight: bold;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n z-index: 2;\n position: relative;\n }\n \n .timeline-circle.completed {\n background-color: #28a745;\n border: 3px solid #1e7e34;\n }\n \n .timeline-circle.pending {\n background-color: #6c757d;\n border: 3px solid #495057;\n }\n \n .timeline-connector {\n position: absolute;\n top: 20px;\n left: 50%;\n right: -50%;\n height: 4px;\n z-index: 1;\n margin-left: 20px;\n margin-right: 20px;\n }\n \n .timeline-connector.completed {\n background-color: #28a745;\n }\n \n .timeline-connector:not(.completed) {\n background-color: #dee2e6;\n }\n \n .timeline-step:last-child .timeline-connector {\n display: none;\n }\n \n .timeline-label {\n margin-top: 15px;\n text-align: center;\n min-height: 60px;\n }\n \n .timeline-label strong {\n display: block;\n font-size: 0.9rem;\n margin-bottom: 4px;\n }\n \n .timeline-label small {\n font-size: 0.75rem;\n line-height: 1.2;\n }\n \n @media (max-width: 768px) {\n .timeline-progress {\n flex-direction: column;\n align-items: stretch;\n }\n \n .timeline-step {\n flex-direction: row;\n align-items: center;\n margin-bottom: 20px;\n }\n \n .timeline-circle {\n margin-right: 15px;\n flex-shrink: 0;\n }\n \n .timeline-connector {\n display: none;\n }\n \n .timeline-label {\n text-align: left;\n margin-top: 0;\n min-height: auto;\n }\n }\n </style><script>\n // Global variables for current logs modal\n let currentTaskId = '';\n let currentWorkerId = Line truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+2
View File
@@ -357,6 +357,8 @@ templ Layout(c *gin.Context, content templ.Component) {
<!-- Bootstrap JS -->
<script src="/static/js/bootstrap.bundle.min.js"></script>
<!-- Modal Alerts JS (replaces native alert/confirm) -->
<script src="/static/js/modal-alerts.js"></script>
<!-- Custom JS -->
<script src="/static/js/admin.js"></script>
</body>
+4 -4
View File
@@ -517,7 +517,7 @@ func Layout(c *gin.Context, content templ.Component) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</small></div></footer><!-- Bootstrap JS --><script src=\"/static/js/bootstrap.bundle.min.js\"></script><!-- Custom JS --><script src=\"/static/js/admin.js\"></script></body></html>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</small></div></footer><!-- Bootstrap JS --><script src=\"/static/js/bootstrap.bundle.min.js\"></script><!-- Modal Alerts JS (replaces native alert/confirm) --><script src=\"/static/js/modal-alerts.js\"></script><!-- Custom JS --><script src=\"/static/js/admin.js\"></script></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -553,7 +553,7 @@ func LoginForm(c *gin.Context, title string, errorMessage string) templ.Componen
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 371, Col: 17}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 373, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
@@ -566,7 +566,7 @@ func LoginForm(c *gin.Context, title string, errorMessage string) templ.Componen
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 385, Col: 57}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 387, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
@@ -584,7 +584,7 @@ func LoginForm(c *gin.Context, title string, errorMessage string) templ.Componen
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 392, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 394, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {