diff options
| author | chrislu <chris.lu@gmail.com> | 2025-07-01 19:59:45 -0700 |
|---|---|---|
| committer | chrislu <chris.lu@gmail.com> | 2025-07-01 19:59:45 -0700 |
| commit | ae1d0a82cee1aac474b832c27556b76f55dd7adc (patch) | |
| tree | 158d99bef96438c109d446e1f9e3bc87c5c29141 | |
| parent | 5c2b2e551376b3955c1bfb91d5b889c1ba506c65 (diff) | |
| download | seaweedfs-ae1d0a82cee1aac474b832c27556b76f55dd7adc.tar.xz seaweedfs-ae1d0a82cee1aac474b832c27556b76f55dd7adc.zip | |
add bucket quota
| -rw-r--r-- | weed/admin/dash/admin_server.go | 69 | ||||
| -rw-r--r-- | weed/admin/dash/bucket_handlers.go | 325 | ||||
| -rw-r--r-- | weed/admin/dash/handler_admin.go | 120 | ||||
| -rw-r--r-- | weed/admin/handlers/handlers.go | 2 | ||||
| -rw-r--r-- | weed/admin/static/js/admin.js | 147 | ||||
| -rw-r--r-- | weed/admin/view/app/s3_buckets.templ | 139 | ||||
| -rw-r--r-- | weed/admin/view/app/s3_buckets_templ.go | 209 |
7 files changed, 796 insertions, 215 deletions
diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index ccfcab6d6..79bce365b 100644 --- a/weed/admin/dash/admin_server.go +++ b/weed/admin/dash/admin_server.go @@ -4,9 +4,7 @@ import ( "context" "fmt" "net/http" - "os" "sort" - "strings" "time" "github.com/seaweedfs/seaweedfs/weed/cluster" @@ -83,6 +81,8 @@ type S3Bucket struct { ObjectCount int64 `json:"object_count"` LastModified time.Time `json:"last_modified"` Status string `json:"status"` + Quota int64 `json:"quota"` // Quota in bytes, 0 means no quota + QuotaEnabled bool `json:"quota_enabled"` // Whether quota is enabled } type S3Object struct { @@ -499,6 +499,15 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) { objectCount = collectionData.FileCount } + // Get quota information from entry + quota := resp.Entry.Quota + quotaEnabled := quota > 0 + if quota < 0 { + // Negative quota means disabled + quota = -quota + quotaEnabled = false + } + bucket := S3Bucket{ Name: bucketName, CreatedAt: time.Unix(resp.Entry.Attributes.Crtime, 0), @@ -506,6 +515,8 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) { ObjectCount: objectCount, LastModified: time.Unix(resp.Entry.Attributes.Mtime, 0), Status: "active", + Quota: quota, + QuotaEnabled: quotaEnabled, } buckets = append(buckets, bucket) } @@ -620,59 +631,7 @@ func (s *AdminServer) listBucketObjects(client filer_pb.SeaweedFilerClient, dire // CreateS3Bucket creates a new S3 bucket func (s *AdminServer) CreateS3Bucket(bucketName string) error { - return s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { - // First ensure /buckets directory exists - _, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ - Directory: "/", - Entry: &filer_pb.Entry{ - Name: "buckets", - IsDirectory: true, - Attributes: &filer_pb.FuseAttributes{ - FileMode: uint32(0755 | os.ModeDir), // Directory mode - Uid: uint32(1000), - Gid: uint32(1000), - Crtime: time.Now().Unix(), - Mtime: time.Now().Unix(), - TtlSec: 0, - }, - }, - }) - // Ignore error if directory already exists - if err != nil && !strings.Contains(err.Error(), "already exists") && !strings.Contains(err.Error(), "existing entry") { - return fmt.Errorf("failed to create /buckets directory: %v", err) - } - - // Check if bucket already exists - _, err = client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ - Directory: "/buckets", - Name: bucketName, - }) - if err == nil { - return fmt.Errorf("bucket %s already exists", bucketName) - } - - // Create bucket directory under /buckets - _, err = client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ - Directory: "/buckets", - Entry: &filer_pb.Entry{ - Name: bucketName, - IsDirectory: true, - Attributes: &filer_pb.FuseAttributes{ - FileMode: uint32(0755 | os.ModeDir), // Directory mode - Uid: uint32(1000), - Gid: uint32(1000), - Crtime: time.Now().Unix(), - Mtime: time.Now().Unix(), - TtlSec: 0, - }, - }, - }) - if err != nil { - return fmt.Errorf("failed to create bucket directory: %v", err) - } - - return nil - }) + return s.CreateS3BucketWithQuota(bucketName, 0, false) } // DeleteS3Bucket deletes an S3 bucket and all its contents diff --git a/weed/admin/dash/bucket_handlers.go b/weed/admin/dash/bucket_handlers.go new file mode 100644 index 000000000..e6edaa217 --- /dev/null +++ b/weed/admin/dash/bucket_handlers.go @@ -0,0 +1,325 @@ +package dash + +import ( + "context" + "fmt" + "net/http" + "os" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" +) + +// S3 Bucket management data structures for templates +type S3BucketsData struct { + Username string `json:"username"` + Buckets []S3Bucket `json:"buckets"` + TotalBuckets int `json:"total_buckets"` + TotalSize int64 `json:"total_size"` + LastUpdated time.Time `json:"last_updated"` +} + +type CreateBucketRequest struct { + Name string `json:"name" binding:"required"` + Region string `json:"region"` + QuotaSize int64 `json:"quota_size"` // Quota size in bytes + QuotaUnit string `json:"quota_unit"` // Unit: MB, GB, TB + QuotaEnabled bool `json:"quota_enabled"` // Whether quota is enabled +} + +// S3 Bucket Management Handlers + +// ShowS3Buckets displays the Object Store buckets management page +func (s *AdminServer) ShowS3Buckets(c *gin.Context) { + username := c.GetString("username") + + buckets, err := s.GetS3Buckets() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get Object Store buckets: " + err.Error()}) + return + } + + // Calculate totals + var totalSize int64 + for _, bucket := range buckets { + totalSize += bucket.Size + } + + data := S3BucketsData{ + Username: username, + Buckets: buckets, + TotalBuckets: len(buckets), + TotalSize: totalSize, + LastUpdated: time.Now(), + } + + c.JSON(http.StatusOK, data) +} + +// ShowBucketDetails displays detailed information about a specific bucket +func (s *AdminServer) ShowBucketDetails(c *gin.Context) { + bucketName := c.Param("bucket") + if bucketName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) + return + } + + details, err := s.GetBucketDetails(bucketName) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bucket details: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, details) +} + +// CreateBucket creates a new S3 bucket +func (s *AdminServer) CreateBucket(c *gin.Context) { + var req CreateBucketRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + + // Validate bucket name (basic validation) + if len(req.Name) < 3 || len(req.Name) > 63 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name must be between 3 and 63 characters"}) + return + } + + // Convert quota to bytes + quotaBytes := convertQuotaToBytes(req.QuotaSize, req.QuotaUnit) + + err := s.CreateS3BucketWithQuota(req.Name, quotaBytes, req.QuotaEnabled) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bucket: " + err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "message": "Bucket created successfully", + "bucket": req.Name, + "quota_size": req.QuotaSize, + "quota_unit": req.QuotaUnit, + "quota_enabled": req.QuotaEnabled, + }) +} + +// UpdateBucketQuota updates the quota settings for a bucket +func (s *AdminServer) UpdateBucketQuota(c *gin.Context) { + bucketName := c.Param("bucket") + if bucketName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) + return + } + + var req struct { + QuotaSize int64 `json:"quota_size"` + QuotaUnit string `json:"quota_unit"` + QuotaEnabled bool `json:"quota_enabled"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + + // Convert quota to bytes + quotaBytes := convertQuotaToBytes(req.QuotaSize, req.QuotaUnit) + + err := s.SetBucketQuota(bucketName, quotaBytes, req.QuotaEnabled) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update bucket quota: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Bucket quota updated successfully", + "bucket": bucketName, + "quota_size": req.QuotaSize, + "quota_unit": req.QuotaUnit, + "quota_enabled": req.QuotaEnabled, + }) +} + +// DeleteBucket deletes an S3 bucket +func (s *AdminServer) DeleteBucket(c *gin.Context) { + bucketName := c.Param("bucket") + if bucketName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) + return + } + + err := s.DeleteS3Bucket(bucketName) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete bucket: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Bucket deleted successfully", + "bucket": bucketName, + }) +} + +// ListBucketsAPI returns the list of buckets as JSON +func (s *AdminServer) ListBucketsAPI(c *gin.Context) { + buckets, err := s.GetS3Buckets() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get buckets: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "buckets": buckets, + "total": len(buckets), + }) +} + +// Helper function to convert quota size and unit to bytes +func convertQuotaToBytes(size int64, unit string) int64 { + if size <= 0 { + return 0 + } + + switch strings.ToUpper(unit) { + case "TB": + return size * 1024 * 1024 * 1024 * 1024 + case "GB": + return size * 1024 * 1024 * 1024 + case "MB": + return size * 1024 * 1024 + default: + // Default to MB if unit is not recognized + return size * 1024 * 1024 + } +} + +// Helper function to convert bytes to appropriate unit and size +func convertBytesToQuota(bytes int64) (int64, string) { + if bytes == 0 { + return 0, "MB" + } + + // Convert to TB if >= 1TB + if bytes >= 1024*1024*1024*1024 && bytes%(1024*1024*1024*1024) == 0 { + return bytes / (1024 * 1024 * 1024 * 1024), "TB" + } + + // Convert to GB if >= 1GB + if bytes >= 1024*1024*1024 && bytes%(1024*1024*1024) == 0 { + return bytes / (1024 * 1024 * 1024), "GB" + } + + // Convert to MB (default) + return bytes / (1024 * 1024), "MB" +} + +// SetBucketQuota sets the quota for a bucket +func (s *AdminServer) SetBucketQuota(bucketName string, quotaBytes int64, quotaEnabled bool) error { + return s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + // Get the current bucket entry + lookupResp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ + Directory: "/buckets", + Name: bucketName, + }) + if err != nil { + return fmt.Errorf("bucket not found: %v", err) + } + + bucketEntry := lookupResp.Entry + + // Determine quota value (negative if disabled) + var quota int64 + if quotaEnabled && quotaBytes > 0 { + quota = quotaBytes + } else if !quotaEnabled && quotaBytes > 0 { + quota = -quotaBytes + } else { + quota = 0 + } + + // Update the quota + bucketEntry.Quota = quota + + // Update the entry + _, err = client.UpdateEntry(context.Background(), &filer_pb.UpdateEntryRequest{ + Directory: "/buckets", + Entry: bucketEntry, + }) + if err != nil { + return fmt.Errorf("failed to update bucket quota: %v", err) + } + + return nil + }) +} + +// CreateS3BucketWithQuota creates a new S3 bucket with quota settings +func (s *AdminServer) CreateS3BucketWithQuota(bucketName string, quotaBytes int64, quotaEnabled bool) error { + return s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + // First ensure /buckets directory exists + _, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ + Directory: "/", + Entry: &filer_pb.Entry{ + Name: "buckets", + IsDirectory: true, + Attributes: &filer_pb.FuseAttributes{ + FileMode: uint32(0755 | os.ModeDir), // Directory mode + Uid: uint32(1000), + Gid: uint32(1000), + Crtime: time.Now().Unix(), + Mtime: time.Now().Unix(), + TtlSec: 0, + }, + }, + }) + // Ignore error if directory already exists + if err != nil && !strings.Contains(err.Error(), "already exists") && !strings.Contains(err.Error(), "existing entry") { + return fmt.Errorf("failed to create /buckets directory: %v", err) + } + + // Check if bucket already exists + _, err = client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ + Directory: "/buckets", + Name: bucketName, + }) + if err == nil { + return fmt.Errorf("bucket %s already exists", bucketName) + } + + // Determine quota value (negative if disabled) + var quota int64 + if quotaEnabled && quotaBytes > 0 { + quota = quotaBytes + } else if !quotaEnabled && quotaBytes > 0 { + quota = -quotaBytes + } else { + quota = 0 + } + + // Create bucket directory under /buckets + _, err = client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ + Directory: "/buckets", + Entry: &filer_pb.Entry{ + Name: bucketName, + IsDirectory: true, + Attributes: &filer_pb.FuseAttributes{ + FileMode: uint32(0755 | os.ModeDir), // Directory mode + Uid: uint32(1000), + Gid: uint32(1000), + Crtime: time.Now().Unix(), + Mtime: time.Now().Unix(), + TtlSec: 0, + }, + Quota: quota, + }, + }) + if err != nil { + return fmt.Errorf("failed to create bucket directory: %v", err) + } + + return nil + }) +} diff --git a/weed/admin/dash/handler_admin.go b/weed/admin/dash/handler_admin.go index 72a59b2ff..a7a783aaf 100644 --- a/weed/admin/dash/handler_admin.go +++ b/weed/admin/dash/handler_admin.go @@ -25,20 +25,6 @@ type AdminData struct { SystemHealth string `json:"system_health"` } -// S3 Bucket management data structures for templates -type S3BucketsData struct { - Username string `json:"username"` - Buckets []S3Bucket `json:"buckets"` - TotalBuckets int `json:"total_buckets"` - TotalSize int64 `json:"total_size"` - LastUpdated time.Time `json:"last_updated"` -} - -type CreateBucketRequest struct { - Name string `json:"name" binding:"required"` - Region string `json:"region"` -} - // Object Store Users management structures type ObjectStoreUser struct { Username string `json:"username"` @@ -128,112 +114,6 @@ func (s *AdminServer) ShowOverview(c *gin.Context) { c.JSON(http.StatusOK, topology) } -// S3 Bucket Management Handlers - -// ShowS3Buckets displays the Object Store buckets management page -func (s *AdminServer) ShowS3Buckets(c *gin.Context) { - username := c.GetString("username") - - buckets, err := s.GetS3Buckets() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get Object Store buckets: " + err.Error()}) - return - } - - // Calculate totals - var totalSize int64 - for _, bucket := range buckets { - totalSize += bucket.Size - } - - data := S3BucketsData{ - Username: username, - Buckets: buckets, - TotalBuckets: len(buckets), - TotalSize: totalSize, - LastUpdated: time.Now(), - } - - c.JSON(http.StatusOK, data) -} - -// ShowBucketDetails displays detailed information about a specific bucket -func (s *AdminServer) ShowBucketDetails(c *gin.Context) { - bucketName := c.Param("bucket") - if bucketName == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) - return - } - - details, err := s.GetBucketDetails(bucketName) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bucket details: " + err.Error()}) - return - } - - c.JSON(http.StatusOK, details) -} - -// CreateBucket creates a new S3 bucket -func (s *AdminServer) CreateBucket(c *gin.Context) { - var req CreateBucketRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) - return - } - - // Validate bucket name (basic validation) - if len(req.Name) < 3 || len(req.Name) > 63 { - c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name must be between 3 and 63 characters"}) - return - } - - err := s.CreateS3Bucket(req.Name) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bucket: " + err.Error()}) - return - } - - c.JSON(http.StatusCreated, gin.H{ - "message": "Bucket created successfully", - "bucket": req.Name, - }) -} - -// DeleteBucket deletes an S3 bucket -func (s *AdminServer) DeleteBucket(c *gin.Context) { - bucketName := c.Param("bucket") - if bucketName == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) - return - } - - err := s.DeleteS3Bucket(bucketName) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete bucket: " + err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "message": "Bucket deleted successfully", - "bucket": bucketName, - }) -} - -// ListBucketsAPI returns buckets as JSON API -func (s *AdminServer) ListBucketsAPI(c *gin.Context) { - buckets, err := s.GetS3Buckets() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "buckets": buckets, - "count": len(buckets), - }) -} - // getMasterNodesStatus checks status of all master nodes func (s *AdminServer) getMasterNodesStatus() []MasterNode { var masterNodes []MasterNode diff --git a/weed/admin/handlers/handlers.go b/weed/admin/handlers/handlers.go index 69b06923f..d57fb0d14 100644 --- a/weed/admin/handlers/handlers.go +++ b/weed/admin/handlers/handlers.go @@ -80,6 +80,7 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, s3Api.POST("/buckets", h.adminServer.CreateBucket) s3Api.DELETE("/buckets/:bucket", h.adminServer.DeleteBucket) s3Api.GET("/buckets/:bucket", h.adminServer.ShowBucketDetails) + s3Api.PUT("/buckets/:bucket/quota", h.adminServer.UpdateBucketQuota) } // File management API routes @@ -126,6 +127,7 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, s3Api.POST("/buckets", h.adminServer.CreateBucket) s3Api.DELETE("/buckets/:bucket", h.adminServer.DeleteBucket) s3Api.GET("/buckets/:bucket", h.adminServer.ShowBucketDetails) + s3Api.PUT("/buckets/:bucket/quota", h.adminServer.UpdateBucketQuota) } // File management API routes diff --git a/weed/admin/static/js/admin.js b/weed/admin/static/js/admin.js index 6aab0cd37..cefba4c9a 100644 --- a/weed/admin/static/js/admin.js +++ b/weed/admin/static/js/admin.js @@ -357,7 +357,48 @@ function initializeEventHandlers() { const bucketName = button.getAttribute('data-bucket-name'); confirmDeleteBucket(bucketName); } + + // Quota management buttons + if (e.target.closest('.quota-btn')) { + const button = e.target.closest('.quota-btn'); + const bucketName = button.getAttribute('data-bucket-name'); + const currentQuota = parseInt(button.getAttribute('data-current-quota')) || 0; + const quotaEnabled = button.getAttribute('data-quota-enabled') === 'true'; + showQuotaModal(bucketName, currentQuota, quotaEnabled); + } }); + + // Quota form submission + const quotaForm = document.getElementById('quotaForm'); + if (quotaForm) { + quotaForm.addEventListener('submit', handleUpdateQuota); + } + + // Enable quota checkbox for create bucket form + const enableQuotaCheckbox = document.getElementById('enableQuota'); + if (enableQuotaCheckbox) { + enableQuotaCheckbox.addEventListener('change', function() { + const quotaSettings = document.getElementById('quotaSettings'); + if (this.checked) { + quotaSettings.style.display = 'block'; + } else { + quotaSettings.style.display = 'none'; + } + }); + } + + // Enable quota checkbox for quota modal + const quotaEnabledCheckbox = document.getElementById('quotaEnabled'); + if (quotaEnabledCheckbox) { + quotaEnabledCheckbox.addEventListener('change', function() { + const quotaSizeSettings = document.getElementById('quotaSizeSettings'); + if (this.checked) { + quotaSizeSettings.style.display = 'block'; + } else { + quotaSizeSettings.style.display = 'none'; + } + }); + } } // Setup form validation @@ -379,7 +420,10 @@ async function handleCreateBucket(event) { const formData = new FormData(form); const bucketData = { name: formData.get('name'), - region: formData.get('region') || 'us-east-1' + region: formData.get('region') || 'us-east-1', + quota_enabled: formData.get('quota_enabled') === 'on', + quota_size: parseInt(formData.get('quota_size')) || 0, + quota_unit: formData.get('quota_unit') || 'MB' }; try { @@ -491,25 +535,27 @@ function exportBucketList() { const rows = Array.from(table.querySelectorAll('tbody tr')); const data = rows.map(row => { const cells = row.querySelectorAll('td'); - if (cells.length < 5) return null; // Skip empty state row + if (cells.length < 6) return null; // Skip empty state row return { name: cells[0].textContent.trim(), created: cells[1].textContent.trim(), objects: cells[2].textContent.trim(), size: cells[3].textContent.trim(), - status: cells[4].textContent.trim() + quota: cells[4].textContent.trim(), + status: cells[5].textContent.trim() }; }).filter(item => item !== null); // Convert to CSV const csv = [ - ['Name', 'Created', 'Objects', 'Size', 'Status'].join(','), + ['Name', 'Created', 'Objects', 'Size', 'Quota', 'Status'].join(','), ...data.map(row => [ row.name, row.created, row.objects, row.size, + row.quota, row.status ].join(',')) ].join('\n'); @@ -1573,4 +1619,97 @@ function getFileIconByName(fileName) { } } +// Quota Management Functions + +// Show quota management modal +function showQuotaModal(bucketName, currentQuotaMB, quotaEnabled) { + document.getElementById('quotaBucketName').value = bucketName; + document.getElementById('quotaEnabled').checked = quotaEnabled; + + // Convert quota to appropriate unit and set values + const quotaBytes = currentQuotaMB * 1024 * 1024; // Convert MB to bytes + const { size, unit } = convertBytesToBestUnit(quotaBytes); + + document.getElementById('quotaSizeMB').value = size; + document.getElementById('quotaUnitMB').value = unit; + + // Show/hide quota size settings based on enabled state + const quotaSizeSettings = document.getElementById('quotaSizeSettings'); + if (quotaEnabled) { + quotaSizeSettings.style.display = 'block'; + } else { + quotaSizeSettings.style.display = 'none'; + } + + const modal = new bootstrap.Modal(document.getElementById('manageQuotaModal')); + modal.show(); +} + +// Convert bytes to the best unit (TB, GB, or MB) +function convertBytesToBestUnit(bytes) { + if (bytes === 0) { + return { size: 0, unit: 'MB' }; + } + + // Check if it's a clean TB value + if (bytes >= 1024 * 1024 * 1024 * 1024 && bytes % (1024 * 1024 * 1024 * 1024) === 0) { + return { size: bytes / (1024 * 1024 * 1024 * 1024), unit: 'TB' }; + } + + // Check if it's a clean GB value + if (bytes >= 1024 * 1024 * 1024 && bytes % (1024 * 1024 * 1024) === 0) { + return { size: bytes / (1024 * 1024 * 1024), unit: 'GB' }; + } + + // Default to MB + return { size: bytes / (1024 * 1024), unit: 'MB' }; +} + +// Handle quota update form submission +async function handleUpdateQuota(event) { + event.preventDefault(); + + const form = event.target; + const formData = new FormData(form); + const bucketName = document.getElementById('quotaBucketName').value; + + const quotaData = { + quota_enabled: formData.get('quota_enabled') === 'on', + quota_size: parseInt(formData.get('quota_size')) || 0, + quota_unit: formData.get('quota_unit') || 'MB' + }; + + try { + const response = await fetch(`/api/s3/buckets/${bucketName}/quota`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(quotaData) + }); + + const result = await response.json(); + + if (response.ok) { + // Success + showAlert('success', `Quota for bucket "${bucketName}" updated successfully!`); + + // Close modal + const modal = bootstrap.Modal.getInstance(document.getElementById('manageQuotaModal')); + modal.hide(); + + // Refresh the page after a short delay + setTimeout(() => { + location.reload(); + }, 1500); + } else { + // Error + showAlert('danger', result.error || 'Failed to update bucket quota'); + } + } catch (error) { + console.error('Error updating bucket quota:', error); + showAlert('danger', 'Network error occurred while updating bucket quota'); + } +} +
\ No newline at end of file diff --git a/weed/admin/view/app/s3_buckets.templ b/weed/admin/view/app/s3_buckets.templ index 620aab20b..0ea07068c 100644 --- a/weed/admin/view/app/s3_buckets.templ +++ b/weed/admin/view/app/s3_buckets.templ @@ -135,6 +135,7 @@ templ S3Buckets(data dash.S3BucketsData) { <th>Created</th> <th>Objects</th> <th>Size</th> + <th>Quota</th> <th>Status</th> <th>Actions</th> </tr> @@ -153,6 +154,24 @@ templ S3Buckets(data dash.S3BucketsData) { <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> <span class={fmt.Sprintf("badge bg-%s", getBucketStatusColor(bucket.Status))}> {bucket.Status} </span> @@ -170,6 +189,14 @@ templ S3Buckets(data dash.S3BucketsData) { <i class="fas fa-eye"></i> </a> <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"> @@ -181,7 +208,7 @@ templ S3Buckets(data dash.S3BucketsData) { } if len(data.Buckets) == 0 { <tr> - <td colspan="6" class="text-center text-muted py-4"> + <td colspan="7" 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> @@ -236,6 +263,36 @@ templ S3Buckets(data dash.S3BucketsData) { 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> <div class="modal-footer"> @@ -275,6 +332,64 @@ templ S3Buckets(data dash.S3BucketsData) { </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> } // Helper functions for template @@ -299,4 +414,26 @@ func countActiveBuckets(buckets []dash.S3Bucket) int { } } return count +} + +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) }
\ No newline at end of file diff --git a/weed/admin/view/app/s3_buckets_templ.go b/weed/admin/view/app/s3_buckets_templ.go index 252dde1c2..ce9b1ac82 100644 --- a/weed/admin/view/app/s3_buckets_templ.go +++ b/weed/admin/view/app/s3_buckets_templ.go @@ -86,7 +86,7 @@ func S3Buckets(data dash.S3BucketsData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</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>Status</th><th>Actions</th></tr></thead> <tbody>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</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>Status</th><th>Actions</th></tr></thead> <tbody>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -107,7 +107,7 @@ func S3Buckets(data dash.S3BucketsData) templ.Component { var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 149, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 150, Col: 64} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -120,7 +120,7 @@ func S3Buckets(data dash.S3BucketsData) templ.Component { var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.CreatedAt.Format("2006-01-02 15:04")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 152, Col: 92} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 153, Col: 92} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { @@ -133,7 +133,7 @@ func S3Buckets(data dash.S3BucketsData) templ.Component { var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", bucket.ObjectCount)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 153, Col: 86} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 154, Col: 86} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { @@ -146,7 +146,7 @@ func S3Buckets(data dash.S3BucketsData) templ.Component { var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(bucket.Size)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 154, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 155, Col: 73} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -156,93 +156,210 @@ func S3Buckets(data dash.S3BucketsData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var11 = []any{fmt.Sprintf("badge bg-%s", getBucketStatusColor(bucket.Status))} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...) + if bucket.Quota > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 = []any{fmt.Sprintf("badge bg-%s", getQuotaStatusColor(bucket.Size, bucket.Quota, bucket.QuotaEnabled))} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span class=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 1, Col: 0} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(bucket.Quota)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 160, Col: 86} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span> ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if bucket.QuotaEnabled { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"small text-muted\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f%% used", float64(bucket.Size)/float64(bucket.Quota)*100)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 164, Col: 139} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"small text-muted\">Disabled</div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<span class=\"text-muted\">No quota</span>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</td><td>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 = []any{fmt.Sprintf("badge bg-%s", getBucketStatusColor(bucket.Status))} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span class=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<span class=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String()) + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var15).String()) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 1, Col: 0} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Status) + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Status) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 157, Col: 66} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 176, Col: 66} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></td><td><div class=\"btn-group btn-group-sm\" role=\"group\"><a href=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</span></td><td><div class=\"btn-group btn-group-sm\" role=\"group\"><a href=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var14 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files?path=/buckets/%s", bucket.Name)) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var14))) + var templ_7745c5c3_Var18 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files?path=/buckets/%s", bucket.Name)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var18))) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" class=\"btn btn-outline-success btn-sm\" title=\"Browse Files\"><i class=\"fas fa-folder-open\"></i></a> <a href=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" class=\"btn btn-outline-success btn-sm\" title=\"Browse Files\"><i class=\"fas fa-folder-open\"></i></a> <a href=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var15 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/s3/buckets/%s", bucket.Name)) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var15))) + var templ_7745c5c3_Var19 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/s3/buckets/%s", bucket.Name)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var19))) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" class=\"btn btn-outline-primary btn-sm\" title=\"View Details\"><i class=\"fas fa-eye\"></i></a> <button type=\"button\" class=\"btn btn-outline-danger btn-sm delete-bucket-btn\" data-bucket-name=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" class=\"btn btn-outline-primary btn-sm\" title=\"View Details\"><i class=\"fas fa-eye\"></i></a> <button type=\"button\" class=\"btn btn-outline-warning btn-sm quota-btn\" data-bucket-name=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Name) + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 174, Col: 89} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 193, Col: 89} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" data-current-quota=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", getQuotaInMB(bucket.Quota))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 194, Col: 125} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" title=\"Delete Bucket\"><i class=\"fas fa-trash\"></i></button></div></td></tr>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" data-quota-enabled=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%t", bucket.QuotaEnabled)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 195, Col: 118} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" 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=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 201, Col: 89} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" title=\"Delete Bucket\"><i class=\"fas fa-trash\"></i></button></div></td></tr>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if len(data.Buckets) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<tr><td colspan=\"6\" 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>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<tr><td colspan=\"7\" 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>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</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: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</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: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, 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/s3_buckets.templ`, Line: 211, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 238, Col: 81} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</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><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>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</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><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>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -274,4 +391,26 @@ func countActiveBuckets(buckets []dash.S3Bucket) int { return count } +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) +} + var _ = templruntime.GeneratedTemplate |
