You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
940 lines
46 KiB
940 lines
46 KiB
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
|
)
|
|
|
|
templ S3Buckets(data dash.S3BucketsData) {
|
|
<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-cube me-2"></i>Object Store Buckets
|
|
</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="#createBucketModal">
|
|
<i class="fas fa-plus me-1"></i>Create Bucket
|
|
</button>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="s3-buckets-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 Buckets
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{fmt.Sprintf("%d", data.TotalBuckets)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-cube 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">
|
|
Total Storage
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{formatBytes(data.TotalSize)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-hdd 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-warning 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-warning text-uppercase mb-1">
|
|
Last Updated
|
|
</div>
|
|
<div class="h6 mb-0 font-weight-bold text-gray-800">
|
|
{data.LastUpdated.Format("15:04:05")}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-clock fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Buckets 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-cube me-2"></i>Object Store Buckets
|
|
</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="#" onclick="exportBucketList()">
|
|
<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" id="bucketsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Created</th>
|
|
<th>Objects</th>
|
|
<th>Size</th>
|
|
<th>Quota</th>
|
|
<th>Versioning</th>
|
|
<th>Object Lock</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
for _, bucket := range data.Buckets {
|
|
<tr>
|
|
<td>
|
|
<a href={templ.SafeURL(fmt.Sprintf("/s3/buckets/%s", bucket.Name))}
|
|
class="text-decoration-none">
|
|
<i class="fas fa-cube me-2"></i>
|
|
{bucket.Name}
|
|
</a>
|
|
</td>
|
|
<td>{bucket.CreatedAt.Format("2006-01-02 15:04")}</td>
|
|
<td>{fmt.Sprintf("%d", bucket.ObjectCount)}</td>
|
|
<td>{formatBytes(bucket.Size)}</td>
|
|
<td>
|
|
if bucket.Quota > 0 {
|
|
<div>
|
|
<span class={fmt.Sprintf("badge bg-%s", getQuotaStatusColor(bucket.Size, bucket.Quota, bucket.QuotaEnabled))}>
|
|
{formatBytes(bucket.Quota)}
|
|
</span>
|
|
if bucket.QuotaEnabled {
|
|
<div class="small text-muted">
|
|
{fmt.Sprintf("%.1f%% used", float64(bucket.Size)/float64(bucket.Quota)*100)}
|
|
</div>
|
|
} else {
|
|
<div class="small text-muted">Disabled</div>
|
|
}
|
|
</div>
|
|
} else {
|
|
<span class="text-muted">No quota</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
if bucket.VersioningEnabled {
|
|
<span class="badge bg-success">
|
|
<i class="fas fa-check me-1"></i>Enabled
|
|
</span>
|
|
} else {
|
|
<span class="badge bg-secondary">
|
|
<i class="fas fa-times me-1"></i>Disabled
|
|
</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
if bucket.ObjectLockEnabled {
|
|
<div>
|
|
<span class="badge bg-warning">
|
|
<i class="fas fa-lock me-1"></i>Enabled
|
|
</span>
|
|
<div class="small text-muted">
|
|
{bucket.ObjectLockMode} • {fmt.Sprintf("%d days", bucket.ObjectLockDuration)}
|
|
</div>
|
|
</div>
|
|
} else {
|
|
<span class="badge bg-secondary">
|
|
<i class="fas fa-unlock me-1"></i>Disabled
|
|
</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm" role="group">
|
|
<a href={templ.SafeURL(fmt.Sprintf("/files?path=/buckets/%s", bucket.Name))}
|
|
class="btn btn-outline-success btn-sm"
|
|
title="Browse Files">
|
|
<i class="fas fa-folder-open"></i>
|
|
</a>
|
|
<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>
|
|
</button>
|
|
<button type="button"
|
|
class="btn btn-outline-warning btn-sm quota-btn"
|
|
data-bucket-name={bucket.Name}
|
|
data-current-quota={fmt.Sprintf("%d", getQuotaInMB(bucket.Quota))}
|
|
data-quota-enabled={fmt.Sprintf("%t", bucket.QuotaEnabled)}
|
|
title="Manage Quota">
|
|
<i class="fas fa-tachometer-alt"></i>
|
|
</button>
|
|
<button type="button"
|
|
class="btn btn-outline-danger btn-sm delete-bucket-btn"
|
|
data-bucket-name={bucket.Name}
|
|
title="Delete Bucket">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
if len(data.Buckets) == 0 {
|
|
<tr>
|
|
<td colspan="8" class="text-center text-muted py-4">
|
|
<i class="fas fa-cube fa-3x mb-3 text-muted"></i>
|
|
<div>
|
|
<h5>No Object Store buckets found</h5>
|
|
<p>Create your first bucket to get started with S3 storage.</p>
|
|
<button type="button" class="btn btn-primary"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#createBucketModal">
|
|
<i class="fas fa-plus me-1"></i>Create Bucket
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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: {data.LastUpdated.Format("2006-01-02 15:04:05")}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Bucket Modal -->
|
|
<div class="modal fade" id="createBucketModal" tabindex="-1" aria-labelledby="createBucketModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="createBucketModalLabel">
|
|
<i class="fas fa-plus me-2"></i>Create New S3 Bucket
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form id="createBucketForm">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label for="bucketName" class="form-label">Bucket Name</label>
|
|
<input type="text" class="form-control" id="bucketName" name="name"
|
|
placeholder="my-bucket-name" required
|
|
pattern="[a-z0-9.-]+"
|
|
title="Bucket name must contain only lowercase letters, numbers, dots, and hyphens">
|
|
<div class="form-text">
|
|
Bucket names must be between 3 and 63 characters, contain only lowercase letters, numbers, dots, and hyphens.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="enableQuota" name="quota_enabled">
|
|
<label class="form-check-label" for="enableQuota">
|
|
Enable Storage Quota
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3" id="quotaSettings" style="display: none;">
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<label for="quotaSize" class="form-label">Quota Size</label>
|
|
<input type="number" class="form-control" id="quotaSize" name="quota_size"
|
|
placeholder="1024" min="1" step="1">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label for="quotaUnit" class="form-label">Unit</label>
|
|
<select class="form-select" id="quotaUnit" name="quota_unit">
|
|
<option value="MB" selected>MB</option>
|
|
<option value="GB">GB</option>
|
|
<option value="TB">TB</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-text">
|
|
Set the maximum storage size for this bucket.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="enableVersioning" name="versioning_enabled">
|
|
<label class="form-check-label" for="enableVersioning">
|
|
Enable Object Versioning
|
|
</label>
|
|
</div>
|
|
<div class="form-text">
|
|
Keep multiple versions of objects in this bucket.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="enableObjectLock" name="object_lock_enabled">
|
|
<label class="form-check-label" for="enableObjectLock">
|
|
Enable Object Lock
|
|
</label>
|
|
</div>
|
|
<div class="form-text">
|
|
Prevent objects from being deleted or overwritten for a specified period. Automatically enables versioning.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3" id="objectLockSettings" style="display: none;">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<label for="objectLockMode" class="form-label">Object Lock Mode</label>
|
|
<select class="form-select" id="objectLockMode" name="object_lock_mode">
|
|
<option value="GOVERNANCE" selected>Governance</option>
|
|
<option value="COMPLIANCE">Compliance</option>
|
|
</select>
|
|
<div class="form-text">
|
|
Governance allows override with special permissions, Compliance is immutable.
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label for="objectLockDuration" class="form-label">Default Retention (days)</label>
|
|
<input type="number" class="form-control" id="objectLockDuration" name="object_lock_duration"
|
|
placeholder="30" min="1" max="36500" step="1">
|
|
<div class="form-text">
|
|
Default retention period for new objects (1-36500 days).
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-plus me-1"></i>Create Bucket
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<div class="modal fade" id="deleteBucketModal" tabindex="-1" aria-labelledby="deleteBucketModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="deleteBucketModalLabel">
|
|
<i class="fas fa-exclamation-triangle me-2 text-warning"></i>Delete Bucket
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Are you sure you want to delete the bucket <strong id="deleteBucketName"></strong>?</p>
|
|
<div class="alert alert-warning">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
<strong>Warning:</strong> This action cannot be undone. All objects in the bucket will be permanently deleted.
|
|
</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" onclick="deleteBucket()">
|
|
<i class="fas fa-trash me-1"></i>Delete Bucket
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Manage Quota Modal -->
|
|
<div class="modal fade" id="manageQuotaModal" tabindex="-1" aria-labelledby="manageQuotaModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="manageQuotaModalLabel">
|
|
<i class="fas fa-tachometer-alt me-2"></i>Manage Bucket Quota
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form id="quotaForm">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Bucket Name</label>
|
|
<input type="text" class="form-control" id="quotaBucketName" readonly>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="quotaEnabled" name="quota_enabled">
|
|
<label class="form-check-label" for="quotaEnabled">
|
|
Enable Storage Quota
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3" id="quotaSizeSettings">
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<label for="quotaSizeMB" class="form-label">Quota Size</label>
|
|
<input type="number" class="form-control" id="quotaSizeMB" name="quota_size"
|
|
placeholder="1024" min="0" step="1">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label for="quotaUnitMB" class="form-label">Unit</label>
|
|
<select class="form-select" id="quotaUnitMB" name="quota_unit">
|
|
<option value="MB" selected>MB</option>
|
|
<option value="GB">GB</option>
|
|
<option value="TB">TB</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-text">
|
|
Set the maximum storage size for this bucket. Set to 0 to remove quota.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-warning">
|
|
<i class="fas fa-save me-1"></i>Update Quota
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</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() {
|
|
const quotaCheckbox = document.getElementById('enableQuota');
|
|
const quotaSettings = document.getElementById('quotaSettings');
|
|
const versioningCheckbox = document.getElementById('enableVersioning');
|
|
const objectLockCheckbox = document.getElementById('enableObjectLock');
|
|
const objectLockSettings = document.getElementById('objectLockSettings');
|
|
const createBucketForm = document.getElementById('createBucketForm');
|
|
|
|
// Toggle quota settings
|
|
quotaCheckbox.addEventListener('change', function() {
|
|
quotaSettings.style.display = this.checked ? 'block' : 'none';
|
|
});
|
|
|
|
// Toggle object lock settings and automatically enable versioning
|
|
objectLockCheckbox.addEventListener('change', function() {
|
|
objectLockSettings.style.display = this.checked ? 'block' : 'none';
|
|
if (this.checked) {
|
|
versioningCheckbox.checked = true;
|
|
versioningCheckbox.disabled = true;
|
|
} else {
|
|
versioningCheckbox.disabled = false;
|
|
}
|
|
});
|
|
|
|
// Handle form submission
|
|
createBucketForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
|
|
const formData = new FormData(this);
|
|
const data = {
|
|
name: formData.get('name'),
|
|
region: formData.get('region') || '',
|
|
quota_size: quotaCheckbox.checked ? parseInt(formData.get('quota_size')) || 0 : 0,
|
|
quota_unit: formData.get('quota_unit') || 'MB',
|
|
quota_enabled: quotaCheckbox.checked,
|
|
versioning_enabled: versioningCheckbox.checked,
|
|
object_lock_enabled: objectLockCheckbox.checked,
|
|
object_lock_mode: formData.get('object_lock_mode') || 'GOVERNANCE',
|
|
object_lock_duration: objectLockCheckbox.checked ? parseInt(formData.get('object_lock_duration')) || 30 : 0
|
|
};
|
|
|
|
// Validate object lock settings
|
|
if (data.object_lock_enabled && data.object_lock_duration <= 0) {
|
|
alert('Please enter a valid retention duration for object lock.');
|
|
return;
|
|
}
|
|
|
|
fetch('/api/s3/buckets', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(data)
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
alert('Error creating bucket: ' + data.error);
|
|
} else {
|
|
alert('Bucket created successfully!');
|
|
// Properly close the modal before reloading
|
|
const createModal = bootstrap.Modal.getInstance(document.getElementById('createBucketModal'));
|
|
if (createModal) {
|
|
createModal.hide();
|
|
}
|
|
setTimeout(() => location.reload(), 500);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Error creating bucket: ' + error.message);
|
|
});
|
|
});
|
|
|
|
// 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;
|
|
|
|
// 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;
|
|
const currentQuota = parseInt(this.dataset.currentQuota);
|
|
const quotaEnabled = this.dataset.quotaEnabled === 'true';
|
|
|
|
document.getElementById('quotaBucketName').value = bucketName;
|
|
document.getElementById('quotaEnabled').checked = quotaEnabled;
|
|
document.getElementById('quotaSizeMB').value = currentQuota;
|
|
|
|
// Toggle quota size settings
|
|
document.getElementById('quotaSizeSettings').style.display = quotaEnabled ? 'block' : 'none';
|
|
|
|
window.currentBucketToUpdate = bucketName;
|
|
|
|
// 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();
|
|
|
|
const formData = new FormData(this);
|
|
const enabled = document.getElementById('quotaEnabled').checked;
|
|
const data = {
|
|
quota_size: enabled ? parseInt(formData.get('quota_size')) || 0 : 0,
|
|
quota_unit: formData.get('quota_unit') || 'MB',
|
|
quota_enabled: enabled
|
|
};
|
|
|
|
fetch(`/api/s3/buckets/${window.currentBucketToUpdate}/quota`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(data)
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
alert('Error updating quota: ' + data.error);
|
|
} else {
|
|
alert('Quota updated successfully!');
|
|
// Properly close the modal before reloading
|
|
if (quotaModalInstance) {
|
|
quotaModalInstance.hide();
|
|
}
|
|
setTimeout(() => location.reload(), 500);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Error updating quota: ' + error.message);
|
|
});
|
|
});
|
|
|
|
// Handle quota enabled checkbox
|
|
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() {
|
|
const bucketName = window.currentBucketToDelete;
|
|
if (!bucketName) return;
|
|
|
|
fetch(`/api/s3/buckets/${bucketName}`, {
|
|
method: 'DELETE'
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
alert('Error deleting bucket: ' + data.error);
|
|
} else {
|
|
alert('Bucket deleted successfully!');
|
|
// Properly close the modal before reloading
|
|
if (deleteModalInstance) {
|
|
deleteModalInstance.hide();
|
|
}
|
|
setTimeout(() => location.reload(), 500);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Error deleting bucket: ' + error.message);
|
|
});
|
|
}
|
|
|
|
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 => {
|
|
const cells = row.querySelectorAll('td');
|
|
if (cells.length > 1) {
|
|
return {
|
|
name: cells[0].textContent.trim(),
|
|
created: cells[1].textContent.trim(),
|
|
objects: cells[2].textContent.trim(),
|
|
size: cells[3].textContent.trim(),
|
|
quota: cells[4].textContent.trim(),
|
|
versioning: cells[5].textContent.trim(),
|
|
objectLock: cells[6].textContent.trim()
|
|
};
|
|
}
|
|
return null;
|
|
}).filter(bucket => bucket !== null);
|
|
|
|
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");
|
|
|
|
const encodedUri = encodeURI(csvContent);
|
|
const link = document.createElement("a");
|
|
link.setAttribute("href", encodedUri);
|
|
link.setAttribute("download", "buckets.csv");
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
</script>
|
|
}
|
|
|
|
// Helper functions for template
|
|
func getQuotaStatusColor(used, quota int64, enabled bool) string {
|
|
if !enabled || quota <= 0 {
|
|
return "secondary"
|
|
}
|
|
|
|
percentage := float64(used) / float64(quota) * 100
|
|
if percentage >= 90 {
|
|
return "danger"
|
|
} else if percentage >= 75 {
|
|
return "warning"
|
|
} else {
|
|
return "success"
|
|
}
|
|
}
|
|
|
|
func getQuotaInMB(quotaBytes int64) int64 {
|
|
if quotaBytes < 0 {
|
|
quotaBytes = -quotaBytes // Handle disabled quotas (negative values)
|
|
}
|
|
return quotaBytes / (1024 * 1024)
|
|
}
|