Admin UI: Add policies (#6968)

* add policies to UI, accessing filer directly

* view, edit policies

* add back buttons for "users" page

* remove unused

* fix ui dark mode when modal is closed

* bucket view details button

* fix browser buttons

* filer action button works

* clean up masters page

* fix volume servers action buttons

* fix collections page action button

* fix properties page

* more obvious

* fix directory creation file mode

* Update file_browser_handlers.go

* directory permission
This commit is contained in:
Chris Lu
2025-07-12 01:13:11 -07:00
committed by GitHub
parent 49d43003e1
commit 687a6a6c1d
41 changed files with 4941 additions and 2383 deletions

View File

@@ -164,22 +164,18 @@ templ ClusterCollections(data dash.ClusterCollectionsData) {
}
</td>
<td>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary btn-sm"
title="View Details">
<i class="fas fa-eye"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm"
title="Edit">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-outline-danger btn-sm"
title="Delete"
data-collection-name={collection.Name}
onclick="confirmDeleteCollection(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<button type="button"
class="btn btn-outline-primary btn-sm"
title="View Details"
data-action="view-details"
data-name={collection.Name}
data-datacenter={collection.DataCenter}
data-volume-count={fmt.Sprintf("%d", collection.VolumeCount)}
data-file-count={fmt.Sprintf("%d", collection.FileCount)}
data-total-size={fmt.Sprintf("%d", collection.TotalSize)}
data-disk-types={formatDiskTypes(collection.DiskTypes)}>
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
}
@@ -209,30 +205,169 @@ templ ClusterCollections(data dash.ClusterCollectionsData) {
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteCollectionModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>Delete Collection
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the collection <strong id="deleteCollectionName"></strong>?</p>
<div class="alert alert-warning">
<i class="fas fa-warning me-2"></i>
This action cannot be undone. All volumes in this collection will be affected.
</div>
</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="confirmDeleteCollection">Delete Collection</button>
</div>
</div>
</div>
</div>
<!-- JavaScript for cluster collections functionality -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Handle collection action buttons
document.addEventListener('click', function(e) {
const button = e.target.closest('[data-action]');
if (!button) return;
const action = button.getAttribute('data-action');
switch(action) {
case 'view-details':
const collectionData = {
name: button.getAttribute('data-name'),
datacenter: button.getAttribute('data-datacenter'),
volumeCount: parseInt(button.getAttribute('data-volume-count')),
fileCount: parseInt(button.getAttribute('data-file-count')),
totalSize: parseInt(button.getAttribute('data-total-size')),
diskTypes: button.getAttribute('data-disk-types')
};
showCollectionDetails(collectionData);
break;
}
});
});
function showCollectionDetails(collection) {
const modalHtml = '<div class="modal fade" id="collectionDetailsModal" tabindex="-1">' +
'<div class="modal-dialog modal-lg">' +
'<div class="modal-content">' +
'<div class="modal-header">' +
'<h5 class="modal-title"><i class="fas fa-layer-group me-2"></i>Collection Details: ' + collection.name + '</h5>' +
'<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' +
'</div>' +
'<div class="modal-body">' +
'<div class="row">' +
'<div class="col-md-6">' +
'<h6 class="text-primary"><i class="fas fa-info-circle me-1"></i>Basic Information</h6>' +
'<table class="table table-sm">' +
'<tr><td><strong>Collection Name:</strong></td><td><code>' + collection.name + '</code></td></tr>' +
'<tr><td><strong>Data Center:</strong></td><td>' +
(collection.datacenter ? '<span class="badge bg-light text-dark">' + collection.datacenter + '</span>' : '<span class="text-muted">N/A</span>') +
'</td></tr>' +
'<tr><td><strong>Disk Types:</strong></td><td>' +
(collection.diskTypes ? collection.diskTypes.split(', ').map(type =>
'<span class="badge bg-' + getDiskTypeBadgeColor(type) + ' me-1">' + type + '</span>'
).join('') : '<span class="text-muted">Unknown</span>') +
'</td></tr>' +
'</table>' +
'</div>' +
'<div class="col-md-6">' +
'<h6 class="text-primary"><i class="fas fa-chart-bar me-1"></i>Storage Statistics</h6>' +
'<table class="table table-sm">' +
'<tr><td><strong>Total Volumes:</strong></td><td>' +
'<div class="d-flex align-items-center">' +
'<i class="fas fa-database me-2 text-muted"></i>' +
'<span>' + collection.volumeCount.toLocaleString() + '</span>' +
'</div>' +
'</td></tr>' +
'<tr><td><strong>Total Files:</strong></td><td>' +
'<div class="d-flex align-items-center">' +
'<i class="fas fa-file me-2 text-muted"></i>' +
'<span>' + collection.fileCount.toLocaleString() + '</span>' +
'</div>' +
'</td></tr>' +
'<tr><td><strong>Total Size:</strong></td><td>' +
'<div class="d-flex align-items-center">' +
'<i class="fas fa-hdd me-2 text-muted"></i>' +
'<span>' + formatBytes(collection.totalSize) + '</span>' +
'</div>' +
'</td></tr>' +
'</table>' +
'</div>' +
'</div>' +
'<div class="row mt-3">' +
'<div class="col-12">' +
'<h6 class="text-primary"><i class="fas fa-link me-1"></i>Quick Actions</h6>' +
'<div class="d-grid gap-2 d-md-flex">' +
'<a href="/cluster/volumes?collection=' + encodeURIComponent(collection.name) + '" class="btn btn-outline-primary">' +
'<i class="fas fa-database me-1"></i>View Volumes' +
'</a>' +
'<a href="/files?collection=' + encodeURIComponent(collection.name) + '" class="btn btn-outline-info">' +
'<i class="fas fa-folder me-1"></i>Browse Files' +
'</a>' +
'</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>';
// Remove existing modal if present
const existingModal = document.getElementById('collectionDetailsModal');
if (existingModal) {
existingModal.remove();
}
// Add modal to body and show
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('collectionDetailsModal'));
modal.show();
// Remove modal when hidden
document.getElementById('collectionDetailsModal').addEventListener('hidden.bs.modal', function() {
this.remove();
});
}
function getDiskTypeBadgeColor(diskType) {
switch(diskType.toLowerCase()) {
case 'ssd':
return 'primary';
case 'hdd':
case '':
return 'secondary';
default:
return 'info';
}
}
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function exportCollections() {
// Simple CSV export of collections list
const rows = Array.from(document.querySelectorAll('#collectionsTable tbody tr')).map(row => {
const cells = row.querySelectorAll('td');
if (cells.length > 1) {
return {
name: cells[0].textContent.trim(),
volumes: cells[1].textContent.trim(),
files: cells[2].textContent.trim(),
size: cells[3].textContent.trim(),
diskTypes: cells[4].textContent.trim()
};
}
return null;
}).filter(row => row !== null);
const csvContent = "data:text/csv;charset=utf-8," +
"Collection Name,Volumes,Files,Size,Disk Types\n" +
rows.map(r => '"' + r.name + '","' + r.volumes + '","' + r.files + '","' + r.size + '","' + r.diskTypes + '"').join("\n");
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "collections.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>
}
func getDiskTypeColor(diskType string) string {

File diff suppressed because one or more lines are too long

View File

@@ -121,6 +121,62 @@ templ ClusterFilers(data dash.ClusterFilersData) {
</div>
</div>
</div>
<!-- JavaScript for cluster filers functionality -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Handle filer action buttons
document.addEventListener('click', function(e) {
const button = e.target.closest('[data-action]');
if (!button) return;
const action = button.getAttribute('data-action');
const address = button.getAttribute('data-address');
if (!address) return;
switch(action) {
case 'open-filer':
openFilerBrowser(address);
break;
}
});
});
function openFilerBrowser(address) {
// Open file browser for specific filer
window.open('/files?filer=' + encodeURIComponent(address), '_blank');
}
function exportFilers() {
// Simple CSV export of filers list
const rows = Array.from(document.querySelectorAll('#filersTable tbody tr')).map(row => {
const cells = row.querySelectorAll('td');
if (cells.length > 1) {
return {
address: cells[0].textContent.trim(),
version: cells[1].textContent.trim(),
datacenter: cells[2].textContent.trim(),
rack: cells[3].textContent.trim(),
created: cells[4].textContent.trim()
};
}
return null;
}).filter(row => row !== null);
const csvContent = "data:text/csv;charset=utf-8," +
"Address,Version,Data Center,Rack,Created At\n" +
rows.map(r => '"' + r.address + '","' + r.version + '","' + r.datacenter + '","' + r.rack + '","' + r.created + '"').join("\n");
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "filers.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>
}

View File

@@ -183,7 +183,7 @@ func ClusterFilers(data dash.ClusterFilersData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</small></div></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</small></div></div></div><!-- JavaScript for cluster filers functionality --><script>\n\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t// Handle filer action buttons\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 address = button.getAttribute('data-address');\n\t\t\t\n\t\t\tif (!address) return;\n\t\t\t\n\t\t\tswitch(action) {\n\t\t\t\tcase 'open-filer':\n\t\t\t\t\topenFilerBrowser(address);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t});\n\t});\n\t\n\tfunction openFilerBrowser(address) {\n\t\t// Open file browser for specific filer\n\t\twindow.open('/files?filer=' + encodeURIComponent(address), '_blank');\n\t}\n\t\n\tfunction exportFilers() {\n\t\t// Simple CSV export of filers list\n\t\tconst rows = Array.from(document.querySelectorAll('#filersTable tbody tr')).map(row => {\n\t\t\tconst cells = row.querySelectorAll('td');\n\t\t\tif (cells.length > 1) {\n\t\t\t\treturn {\n\t\t\t\t\taddress: cells[0].textContent.trim(),\n\t\t\t\t\tversion: cells[1].textContent.trim(),\n\t\t\t\t\tdatacenter: cells[2].textContent.trim(),\n\t\t\t\t\track: cells[3].textContent.trim(),\n\t\t\t\t\tcreated: cells[4].textContent.trim()\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn null;\n\t\t}).filter(row => row !== null);\n\t\t\n\t\tconst csvContent = \"data:text/csv;charset=utf-8,\" + \n\t\t\t\"Address,Version,Data Center,Rack,Created At\\n\" +\n\t\t\trows.map(r => '\"' + r.address + '\",\"' + r.version + '\",\"' + r.datacenter + '\",\"' + r.rack + '\",\"' + r.created + '\"').join(\"\\n\");\n\t\t\n\t\tconst encodedUri = encodeURI(csvContent);\n\t\tconst link = document.createElement(\"a\");\n\t\tlink.setAttribute(\"href\", encodedUri);\n\t\tlink.setAttribute(\"download\", \"filers.csv\");\n\t\tdocument.body.appendChild(link);\n\t\tlink.click();\n\t\tdocument.body.removeChild(link);\n\t}\n\t</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View File

@@ -136,14 +136,15 @@ templ ClusterMasters(data dash.ClusterMastersData) {
}
</td>
<td>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary btn-sm" title="View Details">
<i class="fas fa-eye"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" title="Manage">
<i class="fas fa-cog"></i>
</button>
</div>
<button type="button"
class="btn btn-outline-primary btn-sm"
title="View Details"
data-action="view-details"
data-address={master.Address}
data-leader={fmt.Sprintf("%t", master.IsLeader)}
data-suffrage={master.Suffrage}>
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
}
@@ -170,6 +171,112 @@ templ ClusterMasters(data dash.ClusterMastersData) {
</div>
</div>
</div>
<!-- JavaScript for cluster masters functionality -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Handle master action buttons
document.addEventListener('click', function(e) {
const button = e.target.closest('[data-action]');
if (!button) return;
const action = button.getAttribute('data-action');
const address = button.getAttribute('data-address');
if (!address) return;
switch(action) {
case 'view-details':
const isLeader = button.getAttribute('data-leader') === 'true';
const suffrage = button.getAttribute('data-suffrage');
showMasterDetails(address, isLeader, suffrage);
break;
}
});
});
function showMasterDetails(address, isLeader, suffrage) {
const modalHtml = '<div class="modal fade" id="masterDetailsModal" tabindex="-1">' +
'<div class="modal-dialog modal-lg">' +
'<div class="modal-content">' +
'<div class="modal-header">' +
'<h5 class="modal-title"><i class="fas fa-crown me-2"></i>Master Details: ' + address + '</h5>' +
'<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' +
'</div>' +
'<div class="modal-body">' +
'<div class="row">' +
'<div class="col-md-6">' +
'<h6 class="text-primary"><i class="fas fa-info-circle me-1"></i>Basic Information</h6>' +
'<table class="table table-sm">' +
'<tr><td><strong>Address:</strong></td><td>' + address + '</td></tr>' +
'<tr><td><strong>Role:</strong></td><td>' +
(isLeader ? '<span class="badge bg-warning text-dark"><i class="fas fa-star me-1"></i>Leader</span>' :
'<span class="badge bg-secondary">Follower</span>') + '</td></tr>' +
'<tr><td><strong>Suffrage:</strong></td><td>' + (suffrage || 'N/A') + '</td></tr>' +
'<tr><td><strong>Status:</strong></td><td><span class="badge bg-success">Active</span></td></tr>' +
'</table>' +
'</div>' +
'<div class="col-md-6">' +
'<h6 class="text-primary"><i class="fas fa-link me-1"></i>Quick Actions</h6>' +
'<div class="d-grid gap-2">' +
'<a href="http://' + address + '" target="_blank" class="btn btn-outline-primary">' +
'<i class="fas fa-external-link-alt me-1"></i>Open Master UI' +
'</a>' +
'</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>';
// Remove existing modal if present
const existingModal = document.getElementById('masterDetailsModal');
if (existingModal) {
existingModal.remove();
}
// Add modal to body and show
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('masterDetailsModal'));
modal.show();
// Remove modal when hidden
document.getElementById('masterDetailsModal').addEventListener('hidden.bs.modal', function() {
this.remove();
});
}
function exportMasters() {
// Simple CSV export of masters list
const rows = Array.from(document.querySelectorAll('#mastersTable tbody tr')).map(row => {
const cells = row.querySelectorAll('td');
if (cells.length > 1) {
return {
address: cells[0].textContent.trim(),
role: cells[1].textContent.trim(),
suffrage: cells[2].textContent.trim()
};
}
return null;
}).filter(row => row !== null);
const csvContent = "data:text/csv;charset=utf-8," +
"Address,Role,Suffrage\n" +
rows.map(r => '"' + r.address + '","' + r.role + '","' + r.suffrage + '"').join("\n");
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "masters.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>
}

View File

@@ -154,35 +154,74 @@ func ClusterMasters(data dash.ClusterMastersData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</td><td><div class=\"btn-group btn-group-sm\"><button type=\"button\" class=\"btn btn-outline-primary btn-sm\" title=\"View Details\"><i class=\"fas fa-eye\"></i></button> <button type=\"button\" class=\"btn btn-outline-secondary btn-sm\" title=\"Manage\"><i class=\"fas fa-cog\"></i></button></div></td></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</td><td><button type=\"button\" class=\"btn btn-outline-primary btn-sm\" title=\"View Details\" data-action=\"view-details\" data-address=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(master.Address)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_masters.templ`, Line: 143, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" data-leader=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%t", master.IsLeader))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_masters.templ`, Line: 144, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" data-suffrage=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(master.Suffrage)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_masters.templ`, Line: 145, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\"><i class=\"fas fa-eye\"></i></button></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</tbody></table></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</tbody></table></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"text-center py-5\"><i class=\"fas fa-crown fa-3x text-muted mb-3\"></i><h5 class=\"text-muted\">No Masters Found</h5><p class=\"text-muted\">No master servers are currently available in the cluster.</p></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"text-center py-5\"><i class=\"fas fa-crown fa-3x text-muted mb-3\"></i><h5 class=\"text-muted\">No Masters Found</h5><p class=\"text-muted\">No master servers are currently available in the cluster.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div></div><!-- Last Updated --><div class=\"row\"><div class=\"col-12\"><small class=\"text-muted\"><i class=\"fas fa-clock me-1\"></i> Last updated: ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</div></div><!-- Last Updated --><div class=\"row\"><div class=\"col-12\"><small class=\"text-muted\"><i class=\"fas fa-clock me-1\"></i> Last updated: ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05"))
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_masters.templ`, Line: 168, Col: 67}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_masters.templ`, Line: 169, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</small></div></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</small></div></div></div><!-- JavaScript for cluster masters functionality --><script>\n\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t// Handle master action buttons\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 address = button.getAttribute('data-address');\n\t\t\t\n\t\t\tif (!address) return;\n\t\t\t\n\t\t\tswitch(action) {\n\t\t\t\tcase 'view-details':\n\t\t\t\t\tconst isLeader = button.getAttribute('data-leader') === 'true';\n\t\t\t\t\tconst suffrage = button.getAttribute('data-suffrage');\n\t\t\t\t\tshowMasterDetails(address, isLeader, suffrage);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t});\n\t});\n\t\n\tfunction showMasterDetails(address, isLeader, suffrage) {\n\t\tconst modalHtml = '<div class=\"modal fade\" id=\"masterDetailsModal\" 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-crown me-2\"></i>Master Details: ' + address + '</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\t'<div class=\"row\">' +\n\t\t\t'<div class=\"col-md-6\">' +\n\t\t\t'<h6 class=\"text-primary\"><i class=\"fas fa-info-circle me-1\"></i>Basic Information</h6>' +\n\t\t\t'<table class=\"table table-sm\">' +\n\t\t\t'<tr><td><strong>Address:</strong></td><td>' + address + '</td></tr>' +\n\t\t\t'<tr><td><strong>Role:</strong></td><td>' + \n\t\t\t(isLeader ? '<span class=\"badge bg-warning text-dark\"><i class=\"fas fa-star me-1\"></i>Leader</span>' : \n\t\t\t'<span class=\"badge bg-secondary\">Follower</span>') + '</td></tr>' +\n\t\t\t'<tr><td><strong>Suffrage:</strong></td><td>' + (suffrage || 'N/A') + '</td></tr>' +\n\t\t\t'<tr><td><strong>Status:</strong></td><td><span class=\"badge bg-success\">Active</span></td></tr>' +\n\t\t\t'</table>' +\n\t\t\t'</div>' +\n\t\t\t'<div class=\"col-md-6\">' +\n\t\t\t'<h6 class=\"text-primary\"><i class=\"fas fa-link me-1\"></i>Quick Actions</h6>' +\n\t\t\t'<div class=\"d-grid gap-2\">' +\n\t\t\t'<a href=\"http://' + address + '\" target=\"_blank\" class=\"btn btn-outline-primary\">' +\n\t\t\t'<i class=\"fas fa-external-link-alt me-1\"></i>Open Master UI' +\n\t\t\t'</a>' +\n\t\t\t'</div>' +\n\t\t\t'</div>' +\n\t\t\t'</div>' +\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('masterDetailsModal');\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('masterDetailsModal'));\n\t\tmodal.show();\n\t\t\n\t\t// Remove modal when hidden\n\t\tdocument.getElementById('masterDetailsModal').addEventListener('hidden.bs.modal', function() {\n\t\t\tthis.remove();\n\t\t});\n\t}\n\t\n\tfunction exportMasters() {\n\t\t// Simple CSV export of masters list\n\t\tconst rows = Array.from(document.querySelectorAll('#mastersTable tbody tr')).map(row => {\n\t\t\tconst cells = row.querySelectorAll('td');\n\t\t\tif (cells.length > 1) {\n\t\t\t\treturn {\n\t\t\t\t\taddress: cells[0].textContent.trim(),\n\t\t\t\t\trole: cells[1].textContent.trim(),\n\t\t\t\t\tsuffrage: cells[2].textContent.trim()\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn null;\n\t\t}).filter(row => row !== null);\n\t\t\n\t\tconst csvContent = \"data:text/csv;charset=utf-8,\" + \n\t\t\t\"Address,Role,Suffrage\\n\" +\n\t\t\trows.map(r => '\"' + r.address + '\",\"' + r.role + '\",\"' + r.suffrage + '\"').join(\"\\n\");\n\t\t\n\t\tconst encodedUri = encodeURI(csvContent);\n\t\tconst link = document.createElement(\"a\");\n\t\tlink.setAttribute(\"href\", encodedUri);\n\t\tlink.setAttribute(\"download\", \"masters.csv\");\n\t\tdocument.body.appendChild(link);\n\t\tlink.click();\n\t\tdocument.body.removeChild(link);\n\t}\n\t</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View File

@@ -148,16 +148,22 @@ templ ClusterVolumeServers(data dash.ClusterVolumeServersData) {
</div>
</td>
<td>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary btn-sm"
title="View Details">
<i class="fas fa-eye"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm"
title="Manage">
<i class="fas fa-cog"></i>
</button>
</div>
<button type="button"
class="btn btn-outline-primary btn-sm"
title="View Details"
data-action="view-details"
data-id={host.ID}
data-address={host.Address}
data-public-url={host.PublicURL}
data-datacenter={host.DataCenter}
data-rack={host.Rack}
data-volumes={fmt.Sprintf("%d", host.Volumes)}
data-max-volumes={fmt.Sprintf("%d", host.MaxVolumes)}
data-disk-usage={fmt.Sprintf("%d", host.DiskUsage)}
data-disk-capacity={fmt.Sprintf("%d", host.DiskCapacity)}
data-last-heartbeat={host.LastHeartbeat.Format("2006-01-02 15:04:05")}>
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
}
@@ -184,6 +190,161 @@ templ ClusterVolumeServers(data dash.ClusterVolumeServersData) {
</div>
</div>
</div>
<!-- JavaScript for cluster volume servers functionality -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Handle volume server action buttons
document.addEventListener('click', function(e) {
const button = e.target.closest('[data-action]');
if (!button) return;
const action = button.getAttribute('data-action');
switch(action) {
case 'view-details':
const serverData = {
id: button.getAttribute('data-id'),
address: button.getAttribute('data-address'),
publicUrl: button.getAttribute('data-public-url'),
datacenter: button.getAttribute('data-datacenter'),
rack: button.getAttribute('data-rack'),
volumes: parseInt(button.getAttribute('data-volumes')),
maxVolumes: parseInt(button.getAttribute('data-max-volumes')),
diskUsage: parseInt(button.getAttribute('data-disk-usage')),
diskCapacity: parseInt(button.getAttribute('data-disk-capacity')),
lastHeartbeat: button.getAttribute('data-last-heartbeat')
};
showVolumeServerDetails(serverData);
break;
}
});
});
function showVolumeServerDetails(server) {
const volumePercent = server.maxVolumes > 0 ? Math.round((server.volumes / server.maxVolumes) * 100) : 0;
const diskPercent = server.diskCapacity > 0 ? Math.round((server.diskUsage / server.diskCapacity) * 100) : 0;
const modalHtml = '<div class="modal fade" id="volumeServerDetailsModal" tabindex="-1">' +
'<div class="modal-dialog modal-lg">' +
'<div class="modal-content">' +
'<div class="modal-header">' +
'<h5 class="modal-title"><i class="fas fa-server me-2"></i>Volume Server Details: ' + server.address + '</h5>' +
'<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' +
'</div>' +
'<div class="modal-body">' +
'<div class="row">' +
'<div class="col-md-6">' +
'<h6 class="text-primary"><i class="fas fa-info-circle me-1"></i>Basic Information</h6>' +
'<table class="table table-sm">' +
'<tr><td><strong>Server ID:</strong></td><td><code>' + server.id + '</code></td></tr>' +
'<tr><td><strong>Address:</strong></td><td>' + server.address + '</td></tr>' +
'<tr><td><strong>Public URL:</strong></td><td>' + server.publicUrl + '</td></tr>' +
'<tr><td><strong>Data Center:</strong></td><td><span class="badge bg-light text-dark">' + server.datacenter + '</span></td></tr>' +
'<tr><td><strong>Rack:</strong></td><td><span class="badge bg-light text-dark">' + server.rack + '</span></td></tr>' +
'<tr><td><strong>Last Heartbeat:</strong></td><td>' + server.lastHeartbeat + '</td></tr>' +
'</table>' +
'</div>' +
'<div class="col-md-6">' +
'<h6 class="text-primary"><i class="fas fa-chart-bar me-1"></i>Usage Statistics</h6>' +
'<table class="table table-sm">' +
'<tr><td><strong>Volumes:</strong></td><td>' +
'<div class="d-flex align-items-center">' +
'<div class="progress me-2" style="width: 100px; height: 20px;">' +
'<div class="progress-bar" role="progressbar" style="width: ' + volumePercent + '%"></div>' +
'</div>' +
'<span>' + server.volumes + '/' + server.maxVolumes + ' (' + volumePercent + '%)</span>' +
'</div>' +
'</td></tr>' +
'<tr><td><strong>Disk Usage:</strong></td><td>' +
'<div class="d-flex align-items-center">' +
'<div class="progress me-2" style="width: 100px; height: 20px;">' +
'<div class="progress-bar" role="progressbar" style="width: ' + diskPercent + '%"></div>' +
'</div>' +
'<span>' + formatBytes(server.diskUsage) + '/' + formatBytes(server.diskCapacity) + ' (' + diskPercent + '%)</span>' +
'</div>' +
'</td></tr>' +
'<tr><td><strong>Available Space:</strong></td><td>' + formatBytes(server.diskCapacity - server.diskUsage) + '</td></tr>' +
'</table>' +
'</div>' +
'</div>' +
'<div class="row mt-3">' +
'<div class="col-12">' +
'<h6 class="text-primary"><i class="fas fa-link me-1"></i>Quick Actions</h6>' +
'<div class="d-grid gap-2 d-md-flex">' +
'<a href="http://' + server.publicUrl + '/ui/index.html" target="_blank" class="btn btn-outline-primary">' +
'<i class="fas fa-external-link-alt me-1"></i>Open Volume Server UI' +
'</a>' +
'<a href="/cluster/volumes?server=' + encodeURIComponent(server.address) + '" class="btn btn-outline-info">' +
'<i class="fas fa-database me-1"></i>View Volumes' +
'</a>' +
'</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>';
// Remove existing modal if present
const existingModal = document.getElementById('volumeServerDetailsModal');
if (existingModal) {
existingModal.remove();
}
// Add modal to body and show
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('volumeServerDetailsModal'));
modal.show();
// Remove modal when hidden
document.getElementById('volumeServerDetailsModal').addEventListener('hidden.bs.modal', function() {
this.remove();
});
}
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function exportVolumeServers() {
// Simple CSV export of volume servers list
const rows = Array.from(document.querySelectorAll('#hostsTable tbody tr')).map(row => {
const cells = row.querySelectorAll('td');
if (cells.length > 1) {
return {
id: cells[0].textContent.trim(),
address: cells[1].textContent.trim(),
datacenter: cells[2].textContent.trim(),
rack: cells[3].textContent.trim(),
volumes: cells[4].textContent.trim(),
capacity: cells[5].textContent.trim(),
usage: cells[6].textContent.trim()
};
}
return null;
}).filter(row => row !== null);
const csvContent = "data:text/csv;charset=utf-8," +
"Server ID,Address,Data Center,Rack,Volumes,Capacity,Usage\n" +
rows.map(r => '"' + r.id + '","' + r.address + '","' + r.datacenter + '","' + r.rack + '","' + r.volumes + '","' + r.capacity + '","' + r.usage + '"').join("\n");
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "volume_servers.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>
}

File diff suppressed because one or more lines are too long

View File

@@ -228,7 +228,7 @@ templ FileBrowser(data dash.FileBrowserData) {
}
</td>
<td>
<code class="small">{ entry.Mode }</code>
<code class="small permissions-display" data-mode={ entry.Mode } data-is-directory={ fmt.Sprintf("%t", entry.IsDirectory) }>{ entry.Mode }</code>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
@@ -356,6 +356,380 @@ templ FileBrowser(data dash.FileBrowserData) {
</div>
</div>
</div>
<!-- JavaScript for file browser functionality -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Format permissions in the main table
document.querySelectorAll('.permissions-display').forEach(element => {
const mode = element.getAttribute('data-mode');
const isDirectory = element.getAttribute('data-is-directory') === 'true';
if (mode) {
element.textContent = formatPermissions(mode, isDirectory);
}
});
// Handle file browser action buttons (download, view, properties, delete)
document.addEventListener('click', function(e) {
const button = e.target.closest('[data-action]');
if (!button) return;
const action = button.getAttribute('data-action');
const path = button.getAttribute('data-path');
if (!path) return;
switch(action) {
case 'download':
downloadFile(path);
break;
case 'view':
viewFile(path);
break;
case 'properties':
showFileProperties(path);
break;
case 'delete':
if (confirm('Are you sure you want to delete "' + path + '"?')) {
deleteFile(path);
}
break;
}
});
// Initialize file manager event handlers from admin.js
if (typeof setupFileManagerEventHandlers === 'function') {
setupFileManagerEventHandlers();
}
});
// File browser specific functions
function downloadFile(path) {
// Open download URL in new tab
window.open('/api/files/download?path=' + encodeURIComponent(path), '_blank');
}
function viewFile(path) {
// Open file viewer in new tab
window.open('/api/files/view?path=' + encodeURIComponent(path), '_blank');
}
function showFileProperties(path) {
// Fetch file properties and show in modal
fetch('/api/files/properties?path=' + encodeURIComponent(path))
.then(response => response.json())
.then(data => {
if (data.error) {
alert('Error loading file properties: ' + data.error);
} else {
displayFileProperties(data);
}
})
.catch(error => {
console.error('Error fetching file properties:', error);
alert('Error loading file properties: ' + error.message);
});
}
function displayFileProperties(data) {
// Create a comprehensive modal for file properties
const modalHtml = '<div class="modal fade" id="filePropertiesModal" tabindex="-1">' +
'<div class="modal-dialog modal-lg">' +
'<div class="modal-content">' +
'<div class="modal-header">' +
'<h5 class="modal-title"><i class="fas fa-info-circle me-2"></i>Properties: ' + (data.name || 'Unknown') + '</h5>' +
'<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' +
'</div>' +
'<div class="modal-body">' +
createFilePropertiesContent(data) +
'</div>' +
'<div class="modal-footer">' +
'<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
// Remove existing modal if present
const existingModal = document.getElementById('filePropertiesModal');
if (existingModal) {
existingModal.remove();
}
// Add modal to body and show
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('filePropertiesModal'));
modal.show();
// Remove modal when hidden
document.getElementById('filePropertiesModal').addEventListener('hidden.bs.modal', function() {
this.remove();
});
}
function createFilePropertiesContent(data) {
let html = '<div class="row">' +
'<div class="col-12">' +
'<h6 class="text-primary"><i class="fas fa-file me-1"></i>Basic Information</h6>' +
'<table class="table table-sm">' +
'<tr><td style="width: 120px;"><strong>Name:</strong></td><td>' + (data.name || 'N/A') + '</td></tr>' +
'<tr><td><strong>Full Path:</strong></td><td><code class="text-break">' + (data.full_path || 'N/A') + '</code></td></tr>' +
'<tr><td><strong>Type:</strong></td><td>' + (data.is_directory ? 'Directory' : 'File') + '</td></tr>';
if (!data.is_directory) {
html += '<tr><td><strong>Size:</strong></td><td>' + (data.size_formatted || (data.size ? formatBytes(data.size) : 'N/A')) + '</td></tr>' +
'<tr><td><strong>MIME Type:</strong></td><td>' + (data.mime_type || 'N/A') + '</td></tr>';
}
html += '</table>' +
'</div>' +
'</div>' +
'<div class="row">' +
'<div class="col-md-6">' +
'<h6 class="text-primary"><i class="fas fa-clock me-1"></i>Timestamps</h6>' +
'<table class="table table-sm">';
if (data.modified_time) {
html += '<tr><td><strong>Modified:</strong></td><td>' + data.modified_time + '</td></tr>';
}
if (data.created_time) {
html += '<tr><td><strong>Created:</strong></td><td>' + data.created_time + '</td></tr>';
}
html += '</table>' +
'</div>' +
'<div class="col-md-6">' +
'<h6 class="text-primary"><i class="fas fa-shield-alt me-1"></i>Permissions</h6>' +
'<table class="table table-sm">';
if (data.file_mode) {
const rwxPermissions = formatPermissions(data.file_mode, data.is_directory);
html += '<tr><td><strong>Permissions:</strong></td><td><code>' + rwxPermissions + '</code></td></tr>';
}
if (data.uid !== undefined) {
html += '<tr><td><strong>User ID:</strong></td><td>' + data.uid + '</td></tr>';
}
if (data.gid !== undefined) {
html += '<tr><td><strong>Group ID:</strong></td><td>' + data.gid + '</td></tr>';
}
html += '</table>' +
'</div>' +
'</div>';
// Add advanced info
html += '<div class="row">' +
'<div class="col-12">' +
'<h6 class="text-primary"><i class="fas fa-cog me-1"></i>Advanced</h6>' +
'<table class="table table-sm">';
if (data.chunk_count) {
html += '<tr><td style="width: 120px;"><strong>Chunks:</strong></td><td>' + data.chunk_count + '</td></tr>';
}
if (data.ttl_formatted) {
html += '<tr><td><strong>TTL:</strong></td><td>' + data.ttl_formatted + '</td></tr>';
}
html += '</table>' +
'</div>' +
'</div>';
// Add chunk details if available (show top 5)
if (data.chunks && data.chunks.length > 0) {
const chunksToShow = data.chunks.slice(0, 5);
html += '<div class="row mt-3">' +
'<div class="col-12">' +
'<h6 class="text-primary"><i class="fas fa-puzzle-piece me-1"></i>Chunk Details' +
(data.chunk_count > 5 ? ' (Top 5 of ' + data.chunk_count + ')' : ' (' + data.chunk_count + ')') +
'</h6>' +
'<div class="table-responsive" style="max-height: 200px; overflow-y: auto;">' +
'<table class="table table-sm table-striped">' +
'<thead>' +
'<tr>' +
'<th>File ID</th>' +
'<th>Offset</th>' +
'<th>Size</th>' +
'<th>ETag</th>' +
'</tr>' +
'</thead>' +
'<tbody>';
chunksToShow.forEach(chunk => {
html += '<tr>' +
'<td><code class="small">' + (chunk.file_id || 'N/A') + '</code></td>' +
'<td>' + formatBytes(chunk.offset || 0) + '</td>' +
'<td>' + formatBytes(chunk.size || 0) + '</td>' +
'<td><code class="small">' + (chunk.e_tag || 'N/A') + '</code></td>' +
'</tr>';
});
html += '</tbody>' +
'</table>' +
'</div>' +
'</div>' +
'</div>';
}
// Add extended attributes if present
if (data.extended && Object.keys(data.extended).length > 0) {
html += '<div class="row">' +
'<div class="col-12">' +
'<h6 class="text-primary"><i class="fas fa-tags me-1"></i>Extended Attributes</h6>' +
'<table class="table table-sm">';
for (const [key, value] of Object.entries(data.extended)) {
html += '<tr><td><strong>' + key + ':</strong></td><td>' + value + '</td></tr>';
}
html += '</table>' +
'</div>' +
'</div>';
}
return html;
}
function uploadFile() {
const modal = new bootstrap.Modal(document.getElementById('uploadFileModal'));
modal.show();
}
function toggleSelectAll() {
const selectAllCheckbox = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('.file-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
updateDeleteSelectedButton();
}
function updateDeleteSelectedButton() {
const checkboxes = document.querySelectorAll('.file-checkbox:checked');
const deleteBtn = document.getElementById('deleteSelectedBtn');
if (checkboxes.length > 0) {
deleteBtn.style.display = 'inline-block';
} else {
deleteBtn.style.display = 'none';
}
}
// Helper function to format bytes
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Helper function to format permissions in rwxrwxrwx format
function formatPermissions(mode, isDirectory) {
// Check if mode is already in rwxrwxrwx format (e.g., "drwxr-xr-x" or "-rw-r--r--")
if (mode && (mode.startsWith('d') || mode.startsWith('-') || mode.startsWith('l')) && mode.length === 10) {
return mode; // Already formatted
}
// Convert to number - could be octal string or decimal
let permissions;
if (typeof mode === 'string') {
// Try parsing as octal first, then decimal
if (mode.startsWith('0') && mode.length <= 4) {
permissions = parseInt(mode, 8);
} else {
permissions = parseInt(mode, 10);
}
} else {
permissions = parseInt(mode, 10);
}
if (isNaN(permissions)) {
return isDirectory ? 'drwxr-xr-x' : '-rw-r--r--'; // Default fallback
}
// Handle Go's os.ModeDir conversion
// Go's os.ModeDir is 0x80000000 (2147483648), but Unix S_IFDIR is 0o40000 (16384)
let fileType = '-';
// Check for Go's os.ModeDir flag
if (permissions & 0x80000000) {
fileType = 'd';
}
// Check for standard Unix file type bits
else if ((permissions & 0xF000) === 0x4000) { // S_IFDIR (0o40000)
fileType = 'd';
} else if ((permissions & 0xF000) === 0x8000) { // S_IFREG (0o100000)
fileType = '-';
} else if ((permissions & 0xF000) === 0xA000) { // S_IFLNK (0o120000)
fileType = 'l';
} else if ((permissions & 0xF000) === 0x2000) { // S_IFCHR (0o020000)
fileType = 'c';
} else if ((permissions & 0xF000) === 0x6000) { // S_IFBLK (0o060000)
fileType = 'b';
} else if ((permissions & 0xF000) === 0x1000) { // S_IFIFO (0o010000)
fileType = 'p';
} else if ((permissions & 0xF000) === 0xC000) { // S_IFSOCK (0o140000)
fileType = 's';
}
// Fallback to isDirectory parameter if file type detection fails
else if (isDirectory) {
fileType = 'd';
}
// Permission bits (always use the lower 12 bits for permissions)
const owner = (permissions >> 6) & 7;
const group = (permissions >> 3) & 7;
const others = permissions & 7;
// Convert number to rwx format
function numToRwx(num) {
const r = (num & 4) ? 'r' : '-';
const w = (num & 2) ? 'w' : '-';
const x = (num & 1) ? 'x' : '-';
return r + w + x;
}
return fileType + numToRwx(owner) + numToRwx(group) + numToRwx(others);
}
function exportFileList() {
// Simple CSV export of file list
const rows = Array.from(document.querySelectorAll('#fileTable tbody tr')).map(row => {
const cells = row.querySelectorAll('td');
if (cells.length > 1) {
return {
name: cells[1].textContent.trim(),
size: cells[2].textContent.trim(),
type: cells[3].textContent.trim(),
modified: cells[4].textContent.trim(),
permissions: cells[5].textContent.trim()
};
}
return null;
}).filter(row => row !== null);
const csvContent = "data:text/csv;charset=utf-8," +
"Name,Size,Type,Modified,Permissions\n" +
rows.map(r => '"' + r.name + '","' + r.size + '","' + r.type + '","' + r.modified + '","' + r.permissions + '"').join("\n");
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "files.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// Handle file checkbox changes
document.addEventListener('change', function(e) {
if (e.target.classList.contains('file-checkbox')) {
updateDeleteSelectedButton();
}
});
</script>
}
func countDirectories(entries []dash.FileEntry) int {

File diff suppressed because one or more lines are too long

View File

@@ -317,7 +317,355 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
<!-- JavaScript for user management -->
<script>
// User management functions will be included in admin.js
document.addEventListener('DOMContentLoaded', function() {
// Event delegation for user action buttons
document.addEventListener('click', function(e) {
const button = e.target.closest('[data-action]');
if (!button) return;
const action = button.getAttribute('data-action');
const username = button.getAttribute('data-username');
switch (action) {
case 'show-user-details':
showUserDetails(username);
break;
case 'edit-user':
editUser(username);
break;
case 'manage-access-keys':
manageAccessKeys(username);
break;
case 'delete-user':
deleteUser(username);
break;
}
});
});
// Show user details modal
async function showUserDetails(username) {
try {
const response = await fetch(`/api/users/${username}`);
if (response.ok) {
const user = await response.json();
document.getElementById('userDetailsContent').innerHTML = createUserDetailsContent(user);
const modal = new bootstrap.Modal(document.getElementById('userDetailsModal'));
modal.show();
} else {
showErrorMessage('Failed to load user details');
}
} catch (error) {
console.error('Error loading user details:', error);
showErrorMessage('Failed to load user details');
}
}
// Edit user function
async function editUser(username) {
try {
const response = await fetch(`/api/users/${username}`);
if (response.ok) {
const user = await response.json();
// Populate edit form
document.getElementById('editUsername').value = username;
document.getElementById('editEmail').value = user.email || '';
// Set selected actions
const actionsSelect = document.getElementById('editActions');
Array.from(actionsSelect.options).forEach(option => {
option.selected = user.actions && user.actions.includes(option.value);
});
// Show modal
const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
modal.show();
} else {
showErrorMessage('Failed to load user details');
}
} catch (error) {
console.error('Error loading user:', error);
showErrorMessage('Failed to load user details');
}
}
// Manage access keys function
async function manageAccessKeys(username) {
try {
const response = await fetch(`/api/users/${username}`);
if (response.ok) {
const user = await response.json();
document.getElementById('accessKeysUsername').textContent = username;
document.getElementById('accessKeysContent').innerHTML = createAccessKeysContent(user);
const modal = new bootstrap.Modal(document.getElementById('accessKeysModal'));
modal.show();
} else {
showErrorMessage('Failed to load access keys');
}
} catch (error) {
console.error('Error loading access keys:', error);
showErrorMessage('Failed to load access keys');
}
}
// Delete user function
async function deleteUser(username) {
if (confirm(`Are you sure you want to delete user "${username}"? This action cannot be undone.`)) {
try {
const response = await fetch(`/api/users/${username}`, {
method: 'DELETE'
});
if (response.ok) {
showSuccessMessage('User deleted successfully');
setTimeout(() => window.location.reload(), 1000);
} else {
const error = await response.json();
showErrorMessage('Failed to delete user: ' + (error.error || 'Unknown error'));
}
} catch (error) {
console.error('Error deleting user:', error);
showErrorMessage('Failed to delete user: ' + error.message);
}
}
}
// Handle create user form submission
async function handleCreateUser() {
const form = document.getElementById('createUserForm');
const formData = new FormData(form);
const userData = {
username: formData.get('username'),
email: formData.get('email'),
actions: Array.from(document.getElementById('actions').selectedOptions).map(option => option.value),
generate_key: document.getElementById('generateKey').checked
};
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
});
if (response.ok) {
const result = await response.json();
showSuccessMessage('User created successfully');
// Show the created access key if generated
if (result.user && result.user.access_key) {
showNewAccessKeyModal(result.user);
}
// Close modal and refresh page
const modal = bootstrap.Modal.getInstance(document.getElementById('createUserModal'));
modal.hide();
form.reset();
setTimeout(() => window.location.reload(), 1000);
} else {
const error = await response.json();
showErrorMessage('Failed to create user: ' + (error.error || 'Unknown error'));
}
} catch (error) {
console.error('Error creating user:', error);
showErrorMessage('Failed to create user: ' + error.message);
}
}
// Handle update user form submission
async function handleUpdateUser() {
const username = document.getElementById('editUsername').value;
const formData = new FormData(document.getElementById('editUserForm'));
const userData = {
email: formData.get('email'),
actions: Array.from(document.getElementById('editActions').selectedOptions).map(option => option.value)
};
try {
const response = await fetch(`/api/users/${username}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
});
if (response.ok) {
showSuccessMessage('User updated successfully');
// Close modal and refresh page
const modal = bootstrap.Modal.getInstance(document.getElementById('editUserModal'));
modal.hide();
setTimeout(() => window.location.reload(), 1000);
} else {
const error = await response.json();
showErrorMessage('Failed to update user: ' + (error.error || 'Unknown error'));
}
} catch (error) {
console.error('Error updating user:', error);
showErrorMessage('Failed to update user: ' + error.message);
}
}
// Create user details content
function createUserDetailsContent(user) {
var detailsHtml = '<div class="row">';
detailsHtml += '<div class="col-md-6">';
detailsHtml += '<h6 class="text-muted">Basic Information</h6>';
detailsHtml += '<table class="table table-sm">';
detailsHtml += '<tr><td><strong>Username:</strong></td><td>' + escapeHtml(user.username) + '</td></tr>';
detailsHtml += '<tr><td><strong>Email:</strong></td><td>' + escapeHtml(user.email || 'Not set') + '</td></tr>';
detailsHtml += '</table>';
detailsHtml += '</div>';
detailsHtml += '<div class="col-md-6">';
detailsHtml += '<h6 class="text-muted">Permissions</h6>';
detailsHtml += '<div class="mb-3">';
if (user.actions && user.actions.length > 0) {
detailsHtml += user.actions.map(function(action) {
return '<span class="badge bg-info me-1">' + action + '</span>';
}).join('');
} else {
detailsHtml += '<span class="text-muted">No permissions assigned</span>';
}
detailsHtml += '</div>';
detailsHtml += '<h6 class="text-muted">Access Keys</h6>';
if (user.access_keys && user.access_keys.length > 0) {
detailsHtml += '<div class="mb-2">';
user.access_keys.forEach(function(key) {
detailsHtml += '<div><code class="text-muted">' + key.access_key + '</code></div>';
});
detailsHtml += '</div>';
} else {
detailsHtml += '<p class="text-muted">No access keys</p>';
}
detailsHtml += '</div>';
detailsHtml += '</div>';
return detailsHtml;
}
// Create access keys content
function createAccessKeysContent(user) {
if (!user.access_keys || user.access_keys.length === 0) {
return '<p class="text-muted">No access keys available</p>';
}
var keysHtml = '<div class="table-responsive">';
keysHtml += '<table class="table table-sm">';
keysHtml += '<thead><tr><th>Access Key</th><th>Status</th><th>Actions</th></tr></thead>';
keysHtml += '<tbody>';
user.access_keys.forEach(function(key) {
keysHtml += '<tr>';
keysHtml += '<td><code>' + key.access_key + '</code></td>';
keysHtml += '<td><span class="badge bg-success">Active</span></td>';
keysHtml += '<td>';
keysHtml += '<button class="btn btn-outline-danger btn-sm" onclick="deleteAccessKey(\'' + user.username + '\', \'' + key.access_key + '\')">';
keysHtml += '<i class="fas fa-trash"></i> Delete';
keysHtml += '</button>';
keysHtml += '</td>';
keysHtml += '</tr>';
});
keysHtml += '</tbody>';
keysHtml += '</table>';
keysHtml += '</div>';
return keysHtml;
}
// Create new access key
async function createAccessKey() {
const username = document.getElementById('accessKeysUsername').textContent;
try {
const response = await fetch(`/api/users/${username}/access-keys`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({})
});
if (response.ok) {
const result = await response.json();
showSuccessMessage('Access key created successfully');
// Refresh access keys display
const userResponse = await fetch(`/api/users/${username}`);
if (userResponse.ok) {
const user = await userResponse.json();
document.getElementById('accessKeysContent').innerHTML = createAccessKeysContent(user);
}
} else {
const error = await response.json();
showErrorMessage('Failed to create access key: ' + (error.error || 'Unknown error'));
}
} catch (error) {
console.error('Error creating access key:', error);
showErrorMessage('Failed to create access key: ' + error.message);
}
}
// Delete access key
async function deleteAccessKey(username, accessKey) {
if (confirm('Are you sure you want to delete this access key?')) {
try {
const response = await fetch(`/api/users/${username}/access-keys/${accessKey}`, {
method: 'DELETE'
});
if (response.ok) {
showSuccessMessage('Access key deleted successfully');
// Refresh access keys display
const userResponse = await fetch(`/api/users/${username}`);
if (userResponse.ok) {
const user = await userResponse.json();
document.getElementById('accessKeysContent').innerHTML = createAccessKeysContent(user);
}
} else {
const error = await response.json();
showErrorMessage('Failed to delete access key: ' + (error.error || 'Unknown error'));
}
} catch (error) {
console.error('Error deleting access key:', error);
showErrorMessage('Failed to delete access key: ' + error.message);
}
}
}
// Show new access key modal (when user is created with generated key)
function showNewAccessKeyModal(user) {
// Create a simple alert for now - could be enhanced with a dedicated modal
var message = 'New user created!\n\n';
message += 'Username: ' + user.username + '\n';
message += 'Access Key: ' + user.access_key + '\n';
message += 'Secret Key: ' + user.secret_key + '\n\n';
message += 'Please save these credentials securely.';
alert(message);
}
// Utility functions
function showSuccessMessage(message) {
// Simple implementation - could be enhanced with toast notifications
alert('Success: ' + message);
}
function showErrorMessage(message) {
// Simple implementation - could be enhanced with toast notifications
alert('Error: ' + message);
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,658 @@
package app
import (
"fmt"
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
)
templ Policies(data dash.PoliciesData) {
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">
<i class="fas fa-shield-alt me-2"></i>IAM Policies
</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#createPolicyModal">
<i class="fas fa-plus me-1"></i>Create Policy
</button>
</div>
</div>
</div>
<div id="policies-content">
<!-- Summary Cards -->
<div class="row mb-4">
<div class="col-xl-4 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Total Policies
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{fmt.Sprintf("%d", data.TotalPolicies)}
</div>
</div>
<div class="col-auto">
<i class="fas fa-shield-alt fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
Active Policies
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{fmt.Sprintf("%d", data.TotalPolicies)}
</div>
</div>
<div class="col-auto">
<i class="fas fa-check-circle fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
Last Updated
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{data.LastUpdated.Format("15:04")}
</div>
</div>
<div class="col-auto">
<i class="fas fa-clock fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Policies Table -->
<div class="row">
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-shield-alt me-2"></i>IAM Policies
</h6>
<div class="dropdown no-arrow">
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v fa-sm fa-fw text-gray-400"></i>
</a>
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in">
<div class="dropdown-header">Actions:</div>
<a class="dropdown-item" href="#">
<i class="fas fa-download me-2"></i>Export List
</a>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" width="100%" cellspacing="0">
<thead>
<tr>
<th>Policy Name</th>
<th>Version</th>
<th>Statements</th>
<th>Created</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
for _, policy := range data.Policies {
<tr>
<td>
<strong>{policy.Name}</strong>
</td>
<td>
<span class="badge bg-info">{policy.Document.Version}</span>
</td>
<td>
<span class="badge bg-secondary">{fmt.Sprintf("%d statements", len(policy.Document.Statement))}</span>
</td>
<td>
<small class="text-muted">{policy.CreatedAt.Format("2006-01-02 15:04")}</small>
</td>
<td>
<small class="text-muted">{policy.UpdatedAt.Format("2006-01-02 15:04")}</small>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-info view-policy-btn" title="View Policy" data-policy-name={policy.Name}>
<i class="fas fa-eye"></i>
</button>
<button type="button" class="btn btn-outline-primary edit-policy-btn" title="Edit Policy" data-policy-name={policy.Name}>
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-outline-danger delete-policy-btn" title="Delete Policy" data-policy-name={policy.Name}>
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
}
if len(data.Policies) == 0 {
<tr>
<td colspan="6" class="text-center text-muted py-4">
<i class="fas fa-shield-alt fa-3x mb-3 text-muted"></i>
<div>
<h5>No IAM policies found</h5>
<p>Create your first policy to manage access permissions.</p>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createPolicyModal">
<i class="fas fa-plus me-1"></i>Create Policy
</button>
</div>
</td>
</tr>
}
</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>
// Current policy being viewed/edited
let currentPolicy = null;
// Event listeners for policy actions
document.addEventListener('DOMContentLoaded', function() {
// View policy buttons
document.querySelectorAll('.view-policy-btn').forEach(button => {
button.addEventListener('click', function() {
const policyName = this.getAttribute('data-policy-name');
viewPolicy(policyName);
});
});
// Edit policy buttons
document.querySelectorAll('.edit-policy-btn').forEach(button => {
button.addEventListener('click', function() {
const policyName = this.getAttribute('data-policy-name');
editPolicy(policyName);
});
});
// Delete policy buttons
document.querySelectorAll('.delete-policy-btn').forEach(button => {
button.addEventListener('click', function() {
const policyName = this.getAttribute('data-policy-name');
deletePolicy(policyName);
});
});
// Edit from view button
document.getElementById('editFromViewBtn').addEventListener('click', function() {
if (currentPolicy) {
const viewModal = bootstrap.Modal.getInstance(document.getElementById('viewPolicyModal'));
if (viewModal) viewModal.hide();
editPolicy(currentPolicy.name);
}
});
});
function createPolicy() {
const form = document.getElementById('createPolicyForm');
const formData = new FormData(form);
const policyName = formData.get('name');
const policyDocumentText = formData.get('document');
if (!policyName || !policyDocumentText) {
alert('Please fill in all required fields');
return;
}
let policyDocument;
try {
policyDocument = JSON.parse(policyDocumentText);
} catch (e) {
alert('Invalid JSON in policy document: ' + e.message);
return;
}
const requestData = {
name: policyName,
document: policyDocument
};
fetch('/api/object-store/policies', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Policy created successfully!');
const modal = bootstrap.Modal.getInstance(document.getElementById('createPolicyModal'));
if (modal) modal.hide();
location.reload(); // Refresh the page to show the new policy
} else {
alert('Error creating policy: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error:', error);
alert('Error creating policy: ' + error.message);
});
}
function viewPolicy(policyName) {
// Show the modal first
const modal = new bootstrap.Modal(document.getElementById('viewPolicyModal'));
modal.show();
// Reset content to loading state
document.getElementById('viewPolicyContent').innerHTML = `
<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>
`;
// Fetch policy data
fetch('/api/object-store/policies/' + encodeURIComponent(policyName))
.then(response => {
if (!response.ok) {
throw new Error('Policy not found');
}
return response.json();
})
.then(policy => {
currentPolicy = policy;
displayPolicyDetails(policy);
})
.catch(error => {
console.error('Error:', error);
document.getElementById('viewPolicyContent').innerHTML = `
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i>
Error loading policy: ${error.message}
</div>
`;
});
}
function displayPolicyDetails(policy) {
const content = document.getElementById('viewPolicyContent');
let statementsHtml = '';
if (policy.document && policy.document.Statement) {
statementsHtml = policy.document.Statement.map((stmt, index) => `
<div class="card mb-2">
<div class="card-header py-2">
<h6 class="mb-0">Statement ${index + 1}</h6>
</div>
<div class="card-body py-2">
<div class="row">
<div class="col-md-6">
<strong>Effect:</strong>
<span class="badge ${stmt.Effect === 'Allow' ? 'bg-success' : 'bg-danger'}">${stmt.Effect}</span>
</div>
<div class="col-md-6">
<strong>Actions:</strong> ${Array.isArray(stmt.Action) ? stmt.Action.join(', ') : stmt.Action}
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<strong>Resources:</strong> ${Array.isArray(stmt.Resource) ? stmt.Resource.join(', ') : stmt.Resource}
</div>
</div>
</div>
</div>
`).join('');
}
content.innerHTML = `
<div class="row mb-3">
<div class="col-md-6">
<strong>Policy Name:</strong> ${policy.name || 'Unknown'}
</div>
<div class="col-md-6">
<strong>Version:</strong> <span class="badge bg-info">${policy.document?.Version || 'Unknown'}</span>
</div>
</div>
<div class="mb-3">
<strong>Statements:</strong>
<div class="mt-2">
${statementsHtml || '<p class="text-muted">No statements found</p>'}
</div>
</div>
<div class="mb-3">
<strong>Raw Policy Document:</strong>
<pre class="bg-light p-3 border rounded mt-2"><code>${JSON.stringify(policy.document, null, 2)}</code></pre>
</div>
`;
}
function editPolicy(policyName) {
// Show the modal first
const modal = new bootstrap.Modal(document.getElementById('editPolicyModal'));
modal.show();
// Set policy name
document.getElementById('editPolicyName').value = policyName;
document.getElementById('editPolicyDocument').value = 'Loading...';
// Fetch policy data
fetch('/api/object-store/policies/' + encodeURIComponent(policyName))
.then(response => {
if (!response.ok) {
throw new Error('Policy not found');
}
return response.json();
})
.then(policy => {
currentPolicy = policy;
document.getElementById('editPolicyDocument').value = JSON.stringify(policy.document, null, 2);
})
.catch(error => {
console.error('Error:', error);
alert('Error loading policy for editing: ' + error.message);
const editModal = bootstrap.Modal.getInstance(document.getElementById('editPolicyModal'));
if (editModal) editModal.hide();
});
}
function updatePolicy() {
const policyName = document.getElementById('editPolicyName').value;
const policyDocumentText = document.getElementById('editPolicyDocument').value;
if (!policyName || !policyDocumentText) {
alert('Please fill in all required fields');
return;
}
let policyDocument;
try {
policyDocument = JSON.parse(policyDocumentText);
} catch (e) {
alert('Invalid JSON in policy document: ' + e.message);
return;
}
const requestData = {
document: policyDocument
};
fetch('/api/object-store/policies/' + encodeURIComponent(policyName), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Policy updated successfully!');
const modal = bootstrap.Modal.getInstance(document.getElementById('editPolicyModal'));
if (modal) modal.hide();
location.reload(); // Refresh the page to show the updated policy
} else {
alert('Error updating policy: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error:', error);
alert('Error updating policy: ' + error.message);
});
}
function insertSamplePolicy() {
const samplePolicy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::my-bucket/*"
]
}
]
};
document.getElementById('policyDocument').value = JSON.stringify(samplePolicy, null, 2);
}
function insertSamplePolicyEdit() {
const samplePolicy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::my-bucket/*"
]
}
]
};
document.getElementById('editPolicyDocument').value = JSON.stringify(samplePolicy, null, 2);
}
function validatePolicyDocument() {
const policyText = document.getElementById('policyDocument').value;
validatePolicyJSON(policyText);
}
function validateEditPolicyDocument() {
const policyText = document.getElementById('editPolicyDocument').value;
validatePolicyJSON(policyText);
}
function validatePolicyJSON(policyText) {
if (!policyText) {
alert('Please enter a policy document first');
return;
}
try {
const policy = JSON.parse(policyText);
// Basic validation
if (!policy.Version) {
alert('Policy must have a Version field');
return;
}
if (!policy.Statement || !Array.isArray(policy.Statement)) {
alert('Policy must have a Statement array');
return;
}
alert('Policy document is valid JSON!');
} catch (e) {
alert('Invalid JSON: ' + e.message);
}
}
function deletePolicy(policyName) {
if (confirm('Are you sure you want to delete policy "' + policyName + '"?')) {
fetch('/api/object-store/policies/' + encodeURIComponent(policyName), {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Policy deleted successfully!');
location.reload(); // Refresh the page
} else {
alert('Error deleting policy: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error:', error);
alert('Error deleting policy: ' + error.message);
});
}
}
</script>
}

File diff suppressed because one or more lines are too long

View File

@@ -187,11 +187,12 @@ templ S3Buckets(data dash.S3BucketsData) {
title="Browse Files">
<i class="fas fa-folder-open"></i>
</a>
<a href={templ.SafeURL(fmt.Sprintf("/s3/buckets/%s", bucket.Name))}
class="btn btn-outline-primary btn-sm"
title="View Details">
<button type="button"
class="btn btn-outline-primary btn-sm view-details-btn"
data-bucket-name={bucket.Name}
title="View Details">
<i class="fas fa-eye"></i>
</a>
</button>
<button type="button"
class="btn btn-outline-warning btn-sm quota-btn"
data-bucket-name={bucket.Name}
@@ -442,6 +443,33 @@ templ S3Buckets(data dash.S3BucketsData) {
</div>
</div>
<!-- Bucket Details Modal -->
<div class="modal fade" id="bucketDetailsModal" tabindex="-1" aria-labelledby="bucketDetailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="bucketDetailsModalLabel">
<i class="fas fa-cube me-2"></i>Bucket Details
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="bucketDetailsContent">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<div class="mt-2">Loading bucket details...</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>
<!-- JavaScript for bucket management -->
<script>
document.addEventListener('DOMContentLoaded', function() {
@@ -504,7 +532,12 @@ templ S3Buckets(data dash.S3BucketsData) {
alert('Error creating bucket: ' + data.error);
} else {
alert('Bucket created successfully!');
location.reload();
// Properly close the modal before reloading
const createModal = bootstrap.Modal.getInstance(document.getElementById('createBucketModal'));
if (createModal) {
createModal.hide();
}
setTimeout(() => location.reload(), 500);
}
})
.catch(error => {
@@ -514,16 +547,41 @@ templ S3Buckets(data dash.S3BucketsData) {
});
// Handle delete bucket
let deleteModalInstance = null;
document.querySelectorAll('.delete-bucket-btn').forEach(button => {
button.addEventListener('click', function() {
const bucketName = this.dataset.bucketName;
document.getElementById('deleteBucketName').textContent = bucketName;
window.currentBucketToDelete = bucketName;
new bootstrap.Modal(document.getElementById('deleteBucketModal')).show();
// Dispose of existing modal instance if it exists
if (deleteModalInstance) {
deleteModalInstance.dispose();
}
// Create new modal instance
deleteModalInstance = new bootstrap.Modal(document.getElementById('deleteBucketModal'));
deleteModalInstance.show();
});
});
// Add event listener to properly dispose of delete modal when hidden
document.getElementById('deleteBucketModal').addEventListener('hidden.bs.modal', function() {
if (deleteModalInstance) {
deleteModalInstance.dispose();
deleteModalInstance = null;
}
// Force remove any remaining backdrops
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
backdrop.remove();
});
// Ensure body classes are removed
document.body.classList.remove('modal-open');
document.body.style.removeProperty('padding-right');
});
// Handle quota management
let quotaModalInstance = null;
document.querySelectorAll('.quota-btn').forEach(button => {
button.addEventListener('click', function() {
const bucketName = this.dataset.bucketName;
@@ -538,10 +596,33 @@ templ S3Buckets(data dash.S3BucketsData) {
document.getElementById('quotaSizeSettings').style.display = quotaEnabled ? 'block' : 'none';
window.currentBucketToUpdate = bucketName;
new bootstrap.Modal(document.getElementById('manageQuotaModal')).show();
// Dispose of existing modal instance if it exists
if (quotaModalInstance) {
quotaModalInstance.dispose();
}
// Create new modal instance
quotaModalInstance = new bootstrap.Modal(document.getElementById('manageQuotaModal'));
quotaModalInstance.show();
});
});
// Add event listener to properly dispose of quota modal when hidden
document.getElementById('manageQuotaModal').addEventListener('hidden.bs.modal', function() {
if (quotaModalInstance) {
quotaModalInstance.dispose();
quotaModalInstance = null;
}
// Force remove any remaining backdrops
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
backdrop.remove();
});
// Ensure body classes are removed
document.body.classList.remove('modal-open');
document.body.style.removeProperty('padding-right');
});
// Handle quota form submission
document.getElementById('quotaForm').addEventListener('submit', function(e) {
e.preventDefault();
@@ -567,7 +648,11 @@ templ S3Buckets(data dash.S3BucketsData) {
alert('Error updating quota: ' + data.error);
} else {
alert('Quota updated successfully!');
location.reload();
// Properly close the modal before reloading
if (quotaModalInstance) {
quotaModalInstance.hide();
}
setTimeout(() => location.reload(), 500);
}
})
.catch(error => {
@@ -580,6 +665,74 @@ templ S3Buckets(data dash.S3BucketsData) {
document.getElementById('quotaEnabled').addEventListener('change', function() {
document.getElementById('quotaSizeSettings').style.display = this.checked ? 'block' : 'none';
});
// Handle view details button
let detailsModalInstance = null;
document.querySelectorAll('.view-details-btn').forEach(button => {
button.addEventListener('click', function() {
const bucketName = this.dataset.bucketName;
// Update modal title
document.getElementById('bucketDetailsModalLabel').innerHTML =
'<i class="fas fa-cube me-2"></i>Bucket Details - ' + bucketName;
// Show loading spinner
document.getElementById('bucketDetailsContent').innerHTML =
'<div class="text-center py-4">' +
'<div class="spinner-border text-primary" role="status">' +
'<span class="visually-hidden">Loading...</span>' +
'</div>' +
'<div class="mt-2">Loading bucket details...</div>' +
'</div>';
// Dispose of existing modal instance if it exists
if (detailsModalInstance) {
detailsModalInstance.dispose();
}
// Create new modal instance
detailsModalInstance = new bootstrap.Modal(document.getElementById('bucketDetailsModal'));
detailsModalInstance.show();
// Fetch bucket details
fetch('/api/s3/buckets/' + bucketName)
.then(response => response.json())
.then(data => {
if (data.error) {
document.getElementById('bucketDetailsContent').innerHTML =
'<div class="alert alert-danger">' +
'<i class="fas fa-exclamation-triangle me-2"></i>' +
'Error loading bucket details: ' + data.error +
'</div>';
} else {
displayBucketDetails(data);
}
})
.catch(error => {
console.error('Error fetching bucket details:', error);
document.getElementById('bucketDetailsContent').innerHTML =
'<div class="alert alert-danger">' +
'<i class="fas fa-exclamation-triangle me-2"></i>' +
'Error loading bucket details: ' + error.message +
'</div>';
});
});
});
// Add event listener to properly dispose of details modal when hidden
document.getElementById('bucketDetailsModal').addEventListener('hidden.bs.modal', function() {
if (detailsModalInstance) {
detailsModalInstance.dispose();
detailsModalInstance = null;
}
// Force remove any remaining backdrops
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
backdrop.remove();
});
// Ensure body classes are removed
document.body.classList.remove('modal-open');
document.body.style.removeProperty('padding-right');
});
});
function deleteBucket() {
@@ -595,7 +748,11 @@ templ S3Buckets(data dash.S3BucketsData) {
alert('Error deleting bucket: ' + data.error);
} else {
alert('Bucket deleted successfully!');
location.reload();
// Properly close the modal before reloading
if (deleteModalInstance) {
deleteModalInstance.hide();
}
setTimeout(() => location.reload(), 500);
}
})
.catch(error => {
@@ -604,6 +761,128 @@ templ S3Buckets(data dash.S3BucketsData) {
});
}
function displayBucketDetails(data) {
const bucket = data.bucket;
const objects = data.objects || [];
// Helper function to format bytes
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Helper function to format date
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString();
}
// Generate objects table
let objectsTable = '';
if (objects.length > 0) {
objectsTable = '<div class="table-responsive">' +
'<table class="table table-sm table-striped">' +
'<thead>' +
'<tr>' +
'<th>Object Key</th>' +
'<th>Size</th>' +
'<th>Last Modified</th>' +
'<th>Storage Class</th>' +
'</tr>' +
'</thead>' +
'<tbody>' +
objects.map(obj =>
'<tr>' +
'<td><i class="fas fa-file me-1"></i>' + obj.key + '</td>' +
'<td>' + formatBytes(obj.size) + '</td>' +
'<td>' + formatDate(obj.last_modified) + '</td>' +
'<td><span class="badge bg-primary">' + obj.storage_class + '</span></td>' +
'</tr>'
).join('') +
'</tbody>' +
'</table>' +
'</div>';
} else {
objectsTable = '<div class="text-center py-4 text-muted">' +
'<i class="fas fa-file fa-3x mb-3"></i>' +
'<div>No objects found in this bucket</div>' +
'</div>';
}
const content = '<div class="row">' +
'<div class="col-md-6">' +
'<h6><i class="fas fa-info-circle me-2"></i>Bucket Information</h6>' +
'<table class="table table-sm">' +
'<tr>' +
'<td><strong>Name:</strong></td>' +
'<td>' + bucket.name + '</td>' +
'</tr>' +
'<tr>' +
'<td><strong>Created:</strong></td>' +
'<td>' + formatDate(bucket.created_at) + '</td>' +
'</tr>' +
'<tr>' +
'<td><strong>Last Modified:</strong></td>' +
'<td>' + formatDate(bucket.last_modified) + '</td>' +
'</tr>' +
'<tr>' +
'<td><strong>Total Size:</strong></td>' +
'<td>' + formatBytes(bucket.size) + '</td>' +
'</tr>' +
'<tr>' +
'<td><strong>Object Count:</strong></td>' +
'<td>' + bucket.object_count + '</td>' +
'</tr>' +
'</table>' +
'</div>' +
'<div class="col-md-6">' +
'<h6><i class="fas fa-cogs me-2"></i>Configuration</h6>' +
'<table class="table table-sm">' +
'<tr>' +
'<td><strong>Quota:</strong></td>' +
'<td>' +
(bucket.quota_enabled ?
'<span class="badge bg-success">' + formatBytes(bucket.quota) + '</span>' :
'<span class="badge bg-secondary">Disabled</span>'
) +
'</td>' +
'</tr>' +
'<tr>' +
'<td><strong>Versioning:</strong></td>' +
'<td>' +
(bucket.versioning_enabled ?
'<span class="badge bg-success"><i class="fas fa-check me-1"></i>Enabled</span>' :
'<span class="badge bg-secondary"><i class="fas fa-times me-1"></i>Disabled</span>'
) +
'</td>' +
'</tr>' +
'<tr>' +
'<td><strong>Object Lock:</strong></td>' +
'<td>' +
(bucket.object_lock_enabled ?
'<span class="badge bg-warning"><i class="fas fa-lock me-1"></i>Enabled</span>' +
'<br><small class="text-muted">' + bucket.object_lock_mode + ' • ' + bucket.object_lock_duration + ' days</small>' :
'<span class="badge bg-secondary"><i class="fas fa-unlock me-1"></i>Disabled</span>'
) +
'</td>' +
'</tr>' +
'</table>' +
'</div>' +
'</div>' +
'<hr>' +
'<div class="row">' +
'<div class="col-12">' +
'<h6><i class="fas fa-list me-2"></i>Objects (' + objects.length + ')</h6>' +
objectsTable +
'</div>' +
'</div>';
document.getElementById('bucketDetailsContent').innerHTML = content;
}
function exportBucketList() {
// Simple CSV export
const buckets = Array.from(document.querySelectorAll('#bucketsTable tbody tr')).map(row => {
@@ -624,7 +903,7 @@ templ S3Buckets(data dash.S3BucketsData) {
const csvContent = "data:text/csv;charset=utf-8," +
"Name,Created,Objects,Size,Quota,Versioning,Object Lock\n" +
buckets.map(b => `"${b.name}","${b.created}","${b.objects}","${b.size}","${b.quota}","${b.versioning}","${b.objectLock}"`).join("\n");
buckets.map(b => '"' + b.name + '","' + b.created + '","' + b.objects + '","' + b.size + '","' + b.quota + '","' + b.versioning + '","' + b.objectLock + '"').join("\n");
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");

File diff suppressed because one or more lines are too long