mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-12 01:07:35 +02:00
900 lines
130 KiB
Go
900 lines
130 KiB
Go
// Code generated by templ - DO NOT EDIT.
|
|
|
|
// templ: version: v0.3.1020
|
|
package app
|
|
|
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
|
|
import "github.com/a-h/templ"
|
|
import templruntime "github.com/a-h/templ/runtime"
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
|
)
|
|
|
|
func S3Buckets(data dash.S3BucketsData) templ.Component {
|
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
return templ_7745c5c3_CtxErr
|
|
}
|
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
if !templ_7745c5c3_IsBuffer {
|
|
defer func() {
|
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
if templ_7745c5c3_Err == nil {
|
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
}
|
|
}()
|
|
}
|
|
ctx = templ.InitializeContext(ctx)
|
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
|
if templ_7745c5c3_Var1 == nil {
|
|
templ_7745c5c3_Var1 = templ.NopComponent
|
|
}
|
|
ctx = templ.ClearChildren(ctx)
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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=\"bi bi-box me-2\"></i>Object Store Buckets</h1><div class=\"btn-toolbar mb-2 mb-md-0\"><div class=\"btn-group me-2\"><select class=\"form-select form-select-sm me-2\" id=\"pageSizeSelect\" onchange=\"changePageSize()\" style=\"width: auto;\"><option value=\"50\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if data.PageSize == 50 {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " selected=\"selected\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, ">50 per page</option> <option value=\"100\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if data.PageSize == 100 {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " selected=\"selected\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, ">100 per page</option> <option value=\"200\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if data.PageSize == 200 {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " selected=\"selected\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">200 per page</option> <option value=\"500\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if data.PageSize == 500 {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " selected=\"selected\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ">500 per page</option></select> <button type=\"button\" class=\"btn btn-sm btn-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#createBucketModal\"><i class=\"bi bi-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\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var2 string
|
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalBuckets))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 42, Col: 73}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div></div><div class=\"col-auto\"><i class=\"bi bi-box icon-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\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var3 string
|
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(data.TotalSize))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 62, Col: 64}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></div><div class=\"col-auto\"><i class=\"bi bi-device-hdd icon-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\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var4 string
|
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("15:04:05"))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 83, Col: 72}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></div><div class=\"col-auto\"><i class=\"bi bi-clock icon-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=\"bi bi-box 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=\"bi bi-three-dots-vertical icon-sm icon-fixed-width 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=\"bi bi-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><a href=\"#\" onclick=\"sortTable('name')\" class=\"text-decoration-none text-dark\">Name")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = getSortIcon("name", data.SortBy, data.SortOrder).Render(ctx, templ_7745c5c3_Buffer)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</a></th><th><a href=\"#\" onclick=\"sortTable('owner')\" class=\"text-decoration-none text-dark\">Owner")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = getSortIcon("owner", data.SortBy, data.SortOrder).Render(ctx, templ_7745c5c3_Buffer)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</a></th><th><a href=\"#\" onclick=\"sortTable('created')\" class=\"text-decoration-none text-dark\">Created")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = getSortIcon("created", data.SortBy, data.SortOrder).Render(ctx, templ_7745c5c3_Buffer)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</a></th><th><a href=\"#\" onclick=\"sortTable('logical_size')\" class=\"text-decoration-none text-dark\">Logical Size")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = getSortIcon("logical_size", data.SortBy, data.SortOrder).Render(ctx, templ_7745c5c3_Buffer)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</a></th><th><a href=\"#\" onclick=\"sortTable('physical_size')\" class=\"text-decoration-none text-dark\">Physical Size")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = getSortIcon("physical_size", data.SortBy, data.SortOrder).Render(ctx, templ_7745c5c3_Buffer)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</a></th><th>Quota</th><th>Versioning</th><th>Object Lock</th><th>Lifecycle</th><th>Policy</th><th>Actions</th></tr></thead> <tbody>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
for _, bucket := range data.Buckets {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<tr><td><a href=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var5 templ.SafeURL
|
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(dash.PUrl(ctx, fmt.Sprintf("/files?path=/buckets/%s", bucket.Name)))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 162, Col: 124}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" class=\"text-decoration-none\"><i class=\"bi bi-box me-2\"></i> ")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var6 string
|
|
templ_7745c5c3_Var6, 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: 165, Col: 64}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</a> ")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if bucket.ReadOnly {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<span class=\"badge bg-danger ms-2\" title=\"Bucket is read-only (quota enforcement or manual lock); writes are rejected\"><i class=\"bi bi-lock me-1\"></i>Read-only</span>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</td><td>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if bucket.Owner != "" {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<span class=\"badge bg-info\"><i class=\"bi bi-person me-1\"></i>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var7 string
|
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Owner)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 176, Col: 102}
|
|
}
|
|
_, 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, 24, "</span>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<span class=\"text-muted small\">No owner</span>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</td><td>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
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: 182, Col: 92}
|
|
}
|
|
_, 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, 27, "</td><td><div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var9 string
|
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(bucket.LogicalSize))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 184, Col: 85}
|
|
}
|
|
_, 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, 28, "</div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if bucket.PhysicalSize > 0 && bucket.LogicalSize > 0 && bucket.PhysicalSize > bucket.LogicalSize {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"small text-muted\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var10 string
|
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1fx overhead", float64(bucket.PhysicalSize)/float64(bucket.LogicalSize)))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 187, Col: 144}
|
|
}
|
|
_, 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, 30, "</div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</td><td>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var11 string
|
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(bucket.PhysicalSize))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 191, Col: 81}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</td><td>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if bucket.Quota > 0 {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var12 = []any{fmt.Sprintf("badge bg-%s", getQuotaStatusColor(bucket.LogicalSize, bucket.Quota, bucket.QuotaEnabled))}
|
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<span class=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var13 string
|
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var12).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_7745c5c3_Var13)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var14 string
|
|
templ_7745c5c3_Var14, 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: 196, Col: 86}
|
|
}
|
|
_, 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, 36, "</span> ")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if bucket.QuotaEnabled {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div class=\"small text-muted\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var15 string
|
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f%% used", float64(bucket.LogicalSize)/float64(bucket.Quota)*100))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 200, Col: 146}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div class=\"small text-muted\">Disabled</div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<span class=\"text-muted\">No quota</span>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</td><td>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if bucket.VersioningStatus == "Enabled" {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<span class=\"badge bg-success\"><i class=\"bi bi-check me-1\"></i>Enabled</span>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else if bucket.VersioningStatus == "Suspended" {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<span class=\"badge bg-warning\"><i class=\"bi bi-pause me-1\"></i>Suspended</span>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<span class=\"text-muted\">Not configured</span>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</td><td>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if bucket.ObjectLockEnabled {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<div><span class=\"badge bg-warning\"><i class=\"bi bi-lock me-1\"></i>Enabled</span><div class=\"small text-muted\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var16 string
|
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.ObjectLockMode)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 230, Col: 82}
|
|
}
|
|
_, 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, 48, " • ")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var17 string
|
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d days", bucket.ObjectLockDuration))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 230, Col: 138}
|
|
}
|
|
_, 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, 49, "</div></div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<span class=\"text-muted\">Not configured</span>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</td><td>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if bucket.LifecycleRuleCount > 0 {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<div><button type=\"button\" class=\"badge bg-primary border-0 lifecycle-btn\" data-bucket-name=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var18 string
|
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(bucket.Name)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 241, Col: 93}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" title=\"View Lifecycle Rules\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if bucket.LifecycleRuleCount == 1 {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "1 rule")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else {
|
|
var templ_7745c5c3_Var19 string
|
|
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d rules", bucket.LifecycleRuleCount))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 246, Col: 115}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</button> ")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if bucket.LifecycleEnabledCount < bucket.LifecycleRuleCount {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<div class=\"small text-muted\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var20 string
|
|
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d enabled", bucket.LifecycleEnabledCount))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 251, Col: 120}
|
|
}
|
|
_, 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, 57, "</div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<span class=\"text-muted\">Not configured</span>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</td><td>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if bucket.PolicyStatementCount > 0 {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<button type=\"button\" class=\"badge bg-primary border-0 policy-btn\" data-bucket-name=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var21 string
|
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(bucket.Name)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 262, Col: 89}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" title=\"View Bucket Policy\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if bucket.PolicyStatementCount == 1 {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "1 statement")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else {
|
|
var templ_7745c5c3_Var22 string
|
|
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d statements", bucket.PolicyStatementCount))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 267, 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, 64, "</button>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<span class=\"text-muted\">Not configured</span>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</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_Var23 templ.SafeURL
|
|
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(dash.PUrl(ctx, fmt.Sprintf("/files?path=/buckets/%s", bucket.Name)))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 276, Col: 128}
|
|
}
|
|
_, 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, 67, "\" class=\"btn btn-outline-success btn-sm\" title=\"Browse Files\"><i class=\"bi bi-folder2-open\"></i></a> <button type=\"button\" class=\"btn btn-outline-primary btn-sm view-details-btn\" data-bucket-name=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var24 string
|
|
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(bucket.Name)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 283, Col: 89}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "\" title=\"View Details\"><i class=\"bi bi-eye\"></i></button> <button type=\"button\" class=\"btn btn-outline-secondary btn-sm lifecycle-btn\" data-bucket-name=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var25 string
|
|
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(bucket.Name)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 289, Col: 89}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\" title=\"Lifecycle Rules\"><i class=\"bi bi-recycle\"></i></button> <button type=\"button\" class=\"btn btn-outline-info btn-sm policy-btn\" data-bucket-name=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var26 string
|
|
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(bucket.Name)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 295, Col: 89}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" title=\"Bucket Policy\"><i class=\"bi bi-shield\"></i></button> <button type=\"button\" class=\"btn btn-outline-info btn-sm owner-btn\" data-bucket-name=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var27 string
|
|
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(bucket.Name)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 301, Col: 89}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\" data-current-owner=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var28 string
|
|
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(bucket.Owner)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 302, Col: 92}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\" title=\"Manage Owner\"><i class=\"bi bi-person-gear\"></i></button> <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_Var29 string
|
|
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(bucket.Name)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 308, Col: 89}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\" data-current-quota=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var30 string
|
|
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(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: 309, Col: 125}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" data-quota-enabled=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var31 string
|
|
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%t", bucket.QuotaEnabled))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 310, Col: 118}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\" title=\"Manage Quota\"><i class=\"bi bi-speedometer2\"></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_Var32 string
|
|
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(bucket.Name)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 316, Col: 89}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "\" title=\"Delete Bucket\"><i class=\"bi bi-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, 77, "<tr><td colspan=\"11\" class=\"text-center text-muted py-4\"><i class=\"bi bi-box icon-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=\"bi bi-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, 78, "</tbody></table></div><!-- Pagination Controls -->")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if data.TotalPages > 1 {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<div class=\"d-flex justify-content-between align-items-center mt-3\"><small class=\"text-muted\">Showing ")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var33 string
|
|
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", (data.CurrentPage-1)*data.PageSize+1))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 348, Col: 101}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, " to ")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var34 string
|
|
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", minInt(data.CurrentPage*data.PageSize, data.TotalBuckets)))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 348, Col: 185}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, " of ")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var35 string
|
|
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalBuckets))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 348, Col: 229}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, " buckets</small><nav aria-label=\"Buckets pagination\"><ul class=\"pagination pagination-sm mb-0\"><!-- Previous Button -->")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if data.CurrentPage > 1 {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<li class=\"page-item\"><a class=\"page-link pagination-link\" href=\"#\" data-page=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var36 string
|
|
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", data.CurrentPage-1))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 355, Col: 142}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "\"><i class=\"bi bi-chevron-left\"></i></a></li>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<li class=\"page-item disabled\"><span class=\"page-link\"><i class=\"bi bi-chevron-left\"></i></span></li>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<!-- Page Numbers -->")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
for i := maxInt(1, data.CurrentPage-2); i <= minInt(data.TotalPages, data.CurrentPage+2); i++ {
|
|
if i == data.CurrentPage {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<li class=\"page-item active\"><span class=\"page-link\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var37 string
|
|
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 371, Col: 97}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "</span></li>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "<li class=\"page-item\"><a class=\"page-link pagination-link\" href=\"#\" data-page=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var38 string
|
|
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", i))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 375, Col: 129}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var39 string
|
|
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 375, Col: 152}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</a></li>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "<!-- Next Button -->")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if data.CurrentPage < data.TotalPages {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<li class=\"page-item\"><a class=\"page-link pagination-link\" href=\"#\" data-page=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var40 string
|
|
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", data.CurrentPage+1))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 383, Col: 142}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "\"><i class=\"bi bi-chevron-right\"></i></a></li>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<li class=\"page-item disabled\"><span class=\"page-link\"><i class=\"bi bi-chevron-right\"></i></span></li>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "</ul></nav></div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "</div></div></div></div><!-- Last Updated --><div class=\"row\"><div class=\"col-12\"><small class=\"text-muted\"><i class=\"bi bi-clock me-1\"></i> Last updated: ")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var41 string
|
|
templ_7745c5c3_Var41, 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: 408, Col: 81}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "</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=\"bi bi-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\"><label for=\"bucketOwner\" class=\"form-label\">Owner (Optional)</label> <select class=\"form-select\" id=\"bucketOwner\" name=\"owner\"><option value=\"\">No owner (admin-only access)</option><!-- Options will be populated dynamically when modal opens --></select><div class=\"form-text\">The S3 identity that owns this bucket. Non-admin users can only access buckets they own.</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\"><div class=\"form-check mb-3\"><input class=\"form-check-input\" type=\"checkbox\" id=\"setDefaultRetention\" name=\"set_default_retention\"> <label class=\"form-check-label\" for=\"setDefaultRetention\">Set Default Retention</label><div class=\"form-text\">Apply default retention to all new objects in this bucket.</div></div><div id=\"defaultRetentionSettings\" style=\"display: none;\"><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><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=\"bi bi-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=\"bi bi-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=\"bi bi-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=\"bi bi-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=\"bi bi-speedometer2 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=\"bi bi-floppy 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=\"bi bi-box 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><!-- Bucket Lifecycle Modal --><div class=\"modal fade\" id=\"bucketLifecycleModal\" tabindex=\"-1\" aria-labelledby=\"bucketLifecycleModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog modal-xl\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"bucketLifecycleModalLabel\"><i class=\"bi bi-recycle me-2\"></i>Lifecycle</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><div id=\"bucketLifecycleContent\"></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-outline-danger me-auto\" id=\"lifecycleDeleteAllBtn\"><i class=\"bi bi-trash me-1\"></i>Delete all rules</button> <button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" id=\"lifecycleSaveAllBtn\"><i class=\"bi bi-floppy me-1\"></i>Save</button></div></div></div></div>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = PolicyDatalists().Render(ctx, templ_7745c5c3_Buffer)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "<!-- Bucket Policy Modal --><div class=\"modal fade\" id=\"bucketPolicyModal\" tabindex=\"-1\" aria-labelledby=\"bucketPolicyModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog modal-xl\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"bucketPolicyModalLabel\"><i class=\"bi bi-file-earmark-lock me-2\"></i>Bucket Policy</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><div id=\"bucketPolicyLoading\" 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 policy...</div></div><div id=\"bucketPolicyError\" class=\"alert alert-danger\" style=\"display: none;\"></div><div id=\"bucketPolicyEditorWrapper\" style=\"display: none;\"><p class=\"text-muted small\">Every statement must specify a Principal, and every Resource must refer to this bucket. Leave the document empty and click Delete to remove the policy.</p><ul class=\"nav nav-tabs\" role=\"tablist\"><li class=\"nav-item\" role=\"presentation\"><button class=\"nav-link active\" id=\"bucketPolicyEditorTabBtn\" type=\"button\" data-bs-toggle=\"tab\" data-bs-target=\"#bucketPolicyEditorTab\" role=\"tab\" aria-controls=\"bucketPolicyEditorTab\" aria-selected=\"true\">Editor</button></li><li class=\"nav-item\" role=\"presentation\"><button class=\"nav-link\" id=\"bucketPolicyJsonTabBtn\" type=\"button\" data-bs-toggle=\"tab\" data-bs-target=\"#bucketPolicyJsonTab\" role=\"tab\" aria-controls=\"bucketPolicyJsonTab\" aria-selected=\"false\">JSON</button></li></ul><div class=\"tab-content border border-top-0 rounded-bottom p-3 mb-3\"><div class=\"tab-pane fade show active\" id=\"bucketPolicyEditorTab\" role=\"tabpanel\" aria-labelledby=\"bucketPolicyEditorTabBtn\"><div id=\"bucketPolicyEditorBody\"></div><button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" id=\"bucketPolicyAddStatementBtn\"><i class=\"bi bi-plus me-1\"></i>Add statement</button></div><div class=\"tab-pane fade\" id=\"bucketPolicyJsonTab\" role=\"tabpanel\" aria-labelledby=\"bucketPolicyJsonTabBtn\"><textarea class=\"form-control\" id=\"bucketPolicyDocument\" rows=\"15\" style=\"font-family: monospace;\" spellcheck=\"false\" placeholder='{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":\"*\",\"Action\":\"s3:GetObject\",\"Resource\":\"arn:aws:s3:::bucket-name/*\"}]}'></textarea><div class=\"form-text\">Enter the bucket policy document as JSON</div></div></div><div class=\"mb-3\"><button type=\"button\" class=\"btn btn-outline-info btn-sm\" id=\"bucketPolicyInsertSampleBtn\"><i class=\"bi bi-file-earmark-text me-1\"></i>Use Sample Policy</button></div></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-outline-danger me-auto\" id=\"bucketPolicyDeleteBtn\"><i class=\"bi bi-trash me-1\"></i>Delete</button> <button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" id=\"bucketPolicySaveBtn\"><i class=\"bi bi-floppy me-1\"></i>Save</button></div></div></div></div><!-- Manage Owner Modal --><div class=\"modal fade\" id=\"manageOwnerModal\" tabindex=\"-1\" aria-labelledby=\"manageOwnerModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"manageOwnerModalLabel\"><i class=\"bi bi-person-gear me-2\"></i>Manage Bucket Owner</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><form id=\"ownerForm\"><div class=\"modal-body\"><div class=\"mb-3\"><label class=\"form-label\">Bucket Name</label> <input type=\"text\" class=\"form-control\" id=\"ownerBucketName\" readonly></div><div class=\"mb-3\"><label for=\"bucketOwnerSelect\" class=\"form-label\">Owner</label> <select class=\"form-select\" id=\"bucketOwnerSelect\" name=\"owner\"><option value=\"\">No owner (admin-only access)</option><!-- Options will be populated dynamically --></select><div class=\"form-text\">Select the S3 identity that owns this bucket. Non-admin users can only access buckets they own.</div></div><div id=\"ownerLoadingSpinner\" class=\"text-center py-2\" style=\"display: none;\"><div class=\"spinner-border spinner-border-sm text-primary\" role=\"status\"><span class=\"visually-hidden\">Loading users...</span></div><span class=\"ms-2\">Loading users...</span></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-info\"><i class=\"bi bi-floppy me-1\"></i>Update Owner</button></div></form></div></div></div><!-- JavaScript for bucket management --><script>\n // Global state (shared between DOMContentLoaded handlers and global functions)\n let deleteModalInstance = null;\n let quotaModalInstance = null;\n let ownerModalInstance = null;\n let detailsModalInstance = null;\n let lifecycleModalInstance = null;\n let lifecycleRequestSeq = 0;\n let policyModalInstance = null;\n let policyRequestSeq = 0;\n let policyEditorBucket = null;\n // True only once the bucket-policy GET for the currently open bucket has\n // actually completed successfully. The Save button lives outside the\n // (initially hidden) editor wrapper, so it stays clickable while a load\n // is in flight or has failed; without this guard, Save could commit\n // whatever the editor happened to hold - a stale previous bucket's\n // statements, or a freshly-initialized empty state - onto\n // policyEditorBucket before its own policy ever loaded.\n let bucketPolicyLoaded = false;\n // True while a Save (PUT) or Delete (DELETE) request for the bucket\n // policy is in flight, so a double-click - or Save and Delete fired in\n // quick succession - can't send two overlapping mutations for the same\n // bucket. Cleared on failure so the user can retry; left set on success,\n // since the modal hides and the page reloads shortly after anyway.\n let bucketPolicyMutationInFlight = false;\n\n function setBucketPolicyMutationInFlight(inFlight) {\n bucketPolicyMutationInFlight = inFlight;\n document.getElementById('bucketPolicySaveBtn').disabled = inFlight;\n document.getElementById('bucketPolicyDeleteBtn').disabled = inFlight;\n }\n\n let cachedUsers = null;\n\n // Working copy of the lifecycle rules currently shown in the modal.\n // Edits (add/edit/delete rule) only mutate this local state; nothing is\n // sent to the server until \"Save\" or \"Delete all rules\" is clicked.\n let lifecycleEditor = { bucket: null, rules: [], editingIndex: null, editingRule: null, rawXml: '', hasTransition: false };\n\n function escapeHtml(v) {\n return String(v ?? '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n }\n\n // Shadows admin.js's formatBytes for this page, so it keeps that\n // signature and unit list; the extra guard is for a size the API left\n // out, which the shared one renders as \"NaN undefined\".\n function formatBytes(bytes, decimals = 2) {\n if (!bytes) return '0 Bytes';\n const k = 1024;\n const dm = decimals < 0 ? 0 : decimals;\n const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];\n }\n\n function formatDays(n) {\n return n + (n === 1 ? ' day' : ' days');\n }\n\n // csrfHeaders reads the token the layout stamps into every page\n // (<meta name=\"csrf-token\">) so mutating lifecycle requests pass it the\n // same way the newer admin pages (e.g. s3tables) do.\n function csrfHeaders() {\n const meta = document.querySelector('meta[name=\"csrf-token\"]');\n const token = meta ? (meta.getAttribute('content') || '') : '';\n const headers = { 'Content-Type': 'application/json' };\n if (token) {\n headers['X-CSRF-Token'] = token;\n }\n return headers;\n }\n\n // parseLifecycleResponse reads the body as text before parsing, so a\n // non-JSON response (a proxy's HTML error page, an empty body, plain\n // text from some middleware) surfaces the HTTP status instead of\n // throwing a raw JSON syntax error out of response.json().\n function parseLifecycleResponse(response) {\n return response.text().then(text => {\n let data = {};\n if (text) {\n try {\n data = JSON.parse(text);\n } catch (e) {\n data = { error: response.statusText || ('HTTP ' + response.status) };\n }\n } else if (!response.ok) {\n data = { error: response.statusText || ('HTTP ' + response.status) };\n }\n return { ok: response.ok, data: data };\n });\n }\n\n document.addEventListener('DOMContentLoaded', function() {\n // Add click handlers to pagination links\n document.querySelectorAll('.pagination-link').forEach(link => {\n link.addEventListener('click', function(e) {\n e.preventDefault();\n const page = this.getAttribute('data-page');\n goToPage(page);\n });\n });\n\n // Initialize modal instances once (reuse with show/hide)\n deleteModalInstance = new bootstrap.Modal(document.getElementById('deleteBucketModal'));\n quotaModalInstance = new bootstrap.Modal(document.getElementById('manageQuotaModal'));\n ownerModalInstance = new bootstrap.Modal(document.getElementById('manageOwnerModal'));\n detailsModalInstance = new bootstrap.Modal(document.getElementById('bucketDetailsModal'));\n lifecycleModalInstance = new bootstrap.Modal(document.getElementById('bucketLifecycleModal'));\n policyModalInstance = new bootstrap.Modal(document.getElementById('bucketPolicyModal'));\n\n const quotaCheckbox = document.getElementById('enableQuota');\n const quotaSettings = document.getElementById('quotaSettings');\n const versioningCheckbox = document.getElementById('enableVersioning');\n const objectLockCheckbox = document.getElementById('enableObjectLock');\n const objectLockSettings = document.getElementById('objectLockSettings');\n const setDefaultRetentionCheckbox = document.getElementById('setDefaultRetention');\n const defaultRetentionSettings = document.getElementById('defaultRetentionSettings');\n const createBucketForm = document.getElementById('createBucketForm');\n\n // Toggle quota settings\n quotaCheckbox.addEventListener('change', function() {\n quotaSettings.style.display = this.checked ? 'block' : 'none';\n });\n\n // Toggle object lock settings and automatically enable versioning\n objectLockCheckbox.addEventListener('change', function() {\n objectLockSettings.style.display = this.checked ? 'block' : 'none';\n if (this.checked) {\n versioningCheckbox.checked = true;\n versioningCheckbox.disabled = true;\n } else {\n versioningCheckbox.disabled = false;\n // Reset default retention settings when object lock is disabled\n setDefaultRetentionCheckbox.checked = false;\n defaultRetentionSettings.style.display = 'none';\n }\n });\n\n // Toggle default retention settings\n setDefaultRetentionCheckbox.addEventListener('change', function() {\n defaultRetentionSettings.style.display = this.checked ? 'block' : 'none';\n });\n\n // Populate owner dropdown when create bucket modal opens\n document.getElementById('createBucketModal').addEventListener('show.bs.modal', async function() {\n const ownerSelect = document.getElementById('bucketOwner');\n \n // Only fetch if not already populated\n if (ownerSelect.options.length <= 1) {\n try {\n const response = await fetch(basePath('/api/users'));\n const data = await response.json();\n const users = data.users || [];\n \n users.forEach(user => {\n const option = document.createElement('option');\n option.value = user.username;\n option.textContent = user.username;\n ownerSelect.appendChild(option);\n });\n } catch (error) {\n console.error('Error fetching users for owner dropdown:', error);\n // Reset to default state on error - user can still create bucket without owner\n ownerSelect.innerHTML = '<option value=\"\">No owner (admin-only access)</option>';\n ownerSelect.selectedIndex = 0;\n }\n }\n });\n\n // Handle form submission\n createBucketForm.addEventListener('submit', function(e) {\n e.preventDefault();\n \n const formData = new FormData(this);\n const data = {\n name: formData.get('name'),\n owner: formData.get('owner') || '',\n region: formData.get('region') || '',\n quota_size: quotaCheckbox.checked ? parseInt(formData.get('quota_size')) || 0 : 0,\n quota_unit: formData.get('quota_unit') || 'MB',\n quota_enabled: quotaCheckbox.checked,\n versioning_enabled: versioningCheckbox.checked,\n object_lock_enabled: objectLockCheckbox.checked,\n object_lock_mode: formData.get('object_lock_mode') || 'GOVERNANCE',\n set_default_retention: setDefaultRetentionCheckbox.checked,\n object_lock_duration: setDefaultRetentionCheckbox.checked ? parseInt(formData.get('object_lock_duration')) || 30 : 0\n };\n\n // Validate object lock settings\n if (data.object_lock_enabled && data.set_default_retention && data.object_lock_duration <= 0) {\n alert('Please enter a valid retention duration for object lock.');\n return;\n }\n\n fetch(basePath('/api/s3/buckets'), {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(data)\n })\n .then(response => response.json())\n .then(data => {\n if (data.error) {\n alert('Error creating bucket: ' + data.error);\n } else {\n alert('Bucket created successfully!');\n // Properly close the modal before reloading\n const createModal = bootstrap.Modal.getInstance(document.getElementById('createBucketModal'));\n if (createModal) {\n createModal.hide();\n }\n setTimeout(() => location.reload(), 500);\n }\n })\n .catch(error => {\n console.error('Error:', error);\n alert('Error creating bucket: ' + error.message);\n });\n });\n\n // Handle delete bucket\n const deleteForm = document.getElementById('deleteBucketModal');\n document.querySelectorAll('.delete-bucket-btn').forEach(button => {\n button.addEventListener('click', function() {\n const bucketName = this.dataset.bucketName;\n document.getElementById('deleteBucketName').textContent = bucketName;\n // Store bucket name on modal element instead of global window property\n deleteForm.dataset.bucketName = bucketName;\n deleteModalInstance.show();\n });\n });\n\n // Handle quota management\n const quotaForm = document.getElementById('quotaForm');\n document.querySelectorAll('.quota-btn').forEach(button => {\n button.addEventListener('click', function() {\n const bucketName = this.dataset.bucketName;\n const currentQuota = parseInt(this.dataset.currentQuota);\n const quotaEnabled = this.dataset.quotaEnabled === 'true';\n \n document.getElementById('quotaBucketName').value = bucketName;\n document.getElementById('quotaEnabled').checked = quotaEnabled;\n document.getElementById('quotaSizeMB').value = currentQuota;\n \n // Toggle quota size settings\n document.getElementById('quotaSizeSettings').style.display = quotaEnabled ? 'block' : 'none';\n \n // Store bucket name on form element instead of global window property\n quotaForm.dataset.bucketName = bucketName;\n quotaModalInstance.show();\n });\n });\n\n // Add event listener to properly dispose of quota modal when hidden\n document.getElementById('manageQuotaModal').addEventListener('hidden.bs.modal', function() {\n if (quotaModalInstance) {\n quotaModalInstance.dispose();\n quotaModalInstance = null;\n }\n // Force remove any remaining backdrops\n document.querySelectorAll('.modal-backdrop').forEach(backdrop => {\n backdrop.remove();\n });\n // Ensure body classes are removed\n document.body.classList.remove('modal-open');\n document.body.style.removeProperty('padding-right');\n });\n\n // Handle quota form submission\n document.getElementById('quotaForm').addEventListener('submit', function(e) {\n e.preventDefault();\n \n const bucketName = this.dataset.bucketName;\n if (!bucketName) return;\n\n const formData = new FormData(this);\n const enabled = document.getElementById('quotaEnabled').checked;\n const data = {\n quota_size: enabled ? parseInt(formData.get('quota_size')) || 0 : 0,\n quota_unit: formData.get('quota_unit') || 'MB',\n quota_enabled: enabled\n };\n\n fetch(basePath(`/api/s3/buckets/${bucketName}/quota`), {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(data)\n })\n .then(response => response.json())\n .then(data => {\n if (data.error) {\n alert('Error updating quota: ' + data.error);\n } else {\n alert('Quota updated successfully!');\n // Properly close the modal before reloading\n if (quotaModalInstance) {\n quotaModalInstance.hide();\n }\n setTimeout(() => location.reload(), 500);\n }\n })\n .catch(error => {\n console.error('Error:', error);\n alert('Error updating quota: ' + error.message);\n });\n });\n\n // Handle quota enabled checkbox\n document.getElementById('quotaEnabled').addEventListener('change', function() {\n document.getElementById('quotaSizeSettings').style.display = this.checked ? 'block' : 'none';\n });\n\n // Handle owner management\n const ownerForm = document.getElementById('ownerForm');\n document.querySelectorAll('.owner-btn').forEach(button => {\n button.addEventListener('click', async function() {\n const bucketName = this.dataset.bucketName;\n const currentOwner = this.dataset.currentOwner || '';\n \n document.getElementById('ownerBucketName').value = bucketName;\n // Store bucket name on form element instead of global window property\n ownerForm.dataset.bucketName = bucketName;\n \n // Show loading spinner\n document.getElementById('ownerLoadingSpinner').style.display = 'block';\n document.getElementById('bucketOwnerSelect').disabled = true;\n \n ownerModalInstance.show();\n \n // Fetch users if not cached\n try {\n if (!cachedUsers) {\n const response = await fetch(basePath('/api/users'));\n const data = await response.json();\n cachedUsers = data.users || [];\n }\n \n // Populate the select dropdown\n const select = document.getElementById('bucketOwnerSelect');\n select.innerHTML = '<option value=\"\">No owner (admin-only access)</option>';\n \n cachedUsers.forEach(user => {\n const option = document.createElement('option');\n option.value = user.username;\n option.textContent = user.username;\n if (user.username === currentOwner) {\n option.selected = true;\n }\n select.appendChild(option);\n });\n \n select.disabled = false;\n } catch (error) {\n console.error('Error fetching users:', error);\n alert('Error loading users: ' + error.message);\n // Re-enable select and reset to default on error\n const select = document.getElementById('bucketOwnerSelect');\n select.innerHTML = '<option value=\"\">No owner (admin-only access)</option>';\n select.selectedIndex = 0;\n select.disabled = false;\n } finally {\n document.getElementById('ownerLoadingSpinner').style.display = 'none';\n }\n });\n });\n\n // Handle owner form submission\n document.getElementById('ownerForm').addEventListener('submit', function(e) {\n e.preventDefault();\n \n const bucketName = this.dataset.bucketName;\n if (!bucketName) return;\n\n const owner = document.getElementById('bucketOwnerSelect').value;\n const data = { owner: owner };\n\n fetch(basePath(`/api/s3/buckets/${bucketName}/owner`), {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(data)\n })\n .then(response => response.json())\n .then(data => {\n if (data.error) {\n alert('Error updating owner: ' + data.error);\n } else {\n alert('Bucket owner updated successfully!');\n // Properly close the modal before reloading\n if (ownerModalInstance) {\n ownerModalInstance.hide();\n }\n setTimeout(() => location.reload(), 500);\n }\n })\n .catch(error => {\n console.error('Error:', error);\n alert('Error updating owner: ' + error.message);\n });\n });\n\n // Handle view details button\n document.querySelectorAll('.view-details-btn').forEach(button => {\n button.addEventListener('click', function() {\n const bucketName = this.dataset.bucketName;\n \n // Update modal title\n document.getElementById('bucketDetailsModalLabel').innerHTML = \n '<i class=\"bi bi-box me-2\"></i>Bucket Details - ' + bucketName;\n \n // Show loading spinner\n document.getElementById('bucketDetailsContent').innerHTML = \n '<div class=\"text-center py-4\">' +\n '<div class=\"spinner-border text-primary\" role=\"status\">' +\n '<span class=\"visually-hidden\">Loading...</span>' +\n '<\\\\/div>' +\n '<div class=\"mt-2\">Loading bucket details...</div>' +\n '<\\\\/div>';\n \n detailsModalInstance.show();\n \n // Fetch bucket details\n fetch(basePath('/api/s3/buckets/' + bucketName))\n .then(response => response.json())\n .then(data => {\n if (data.error) {\n document.getElementById('bucketDetailsContent').innerHTML = \n '<div class=\"alert alert-danger\">' +\n '<i class=\"bi bi-exclamation-triangle me-2\"></i>' +\n 'Error loading bucket details: ' + data.error +\n '<\\\\/div>';\n } else {\n displayBucketDetails(data);\n }\n })\n .catch(error => {\n console.error('Error fetching bucket details:', error);\n document.getElementById('bucketDetailsContent').innerHTML = \n '<div class=\"alert alert-danger\">' +\n '<i class=\"bi bi-exclamation-triangle me-2\"></i>' +\n 'Error loading bucket details: ' + error.message +\n '<\\\\/div>';\n });\n });\n });\n\n // Lifecycle buttons: column badge and action button\n document.querySelectorAll('.lifecycle-btn').forEach(button => {\n button.addEventListener('click', function() {\n const bucketName = this.dataset.bucketName;\n\n // Reset the working copy until the GET below repopulates it,\n // so Save/Delete-all can't act on the previous bucket's rules.\n lifecycleEditor = { bucket: null, rules: [], editingIndex: null, editingRule: null, rawXml: '', hasTransition: false };\n\n document.getElementById('bucketLifecycleModalLabel').innerHTML =\n '<i class=\"bi bi-recycle me-2\"></i>Lifecycle - ' + bucketName;\n\n document.getElementById('bucketLifecycleContent').innerHTML =\n '<div class=\"text-center py-4\">' +\n '<div class=\"spinner-border text-primary\" role=\"status\">' +\n '<span class=\"visually-hidden\">Loading...</span>' +\n '<\\/div>' +\n '<div class=\"mt-2\">Loading lifecycle rules...</div>' +\n '<\\/div>';\n\n lifecycleModalInstance.show();\n\n // Drop responses that arrive after another bucket was opened\n const requestSeq = ++lifecycleRequestSeq;\n fetch(basePath('/api/s3/buckets/' + bucketName + '/lifecycle'))\n .then(parseLifecycleResponse)\n .then(({ ok, data }) => {\n if (requestSeq !== lifecycleRequestSeq) return;\n if (!ok || data.error) {\n document.getElementById('bucketLifecycleContent').innerHTML =\n '<div class=\"alert alert-danger\">' +\n '<i class=\"bi bi-exclamation-triangle me-2\"></i>' +\n 'Error loading lifecycle rules: ' + (data.error || 'unknown error') +\n '<\\/div>';\n } else {\n displayBucketLifecycle(data);\n }\n })\n .catch(error => {\n if (requestSeq !== lifecycleRequestSeq) return;\n console.error('Error fetching bucket lifecycle:', error);\n document.getElementById('bucketLifecycleContent').innerHTML =\n '<div class=\"alert alert-danger\">' +\n '<i class=\"bi bi-exclamation-triangle me-2\"></i>' +\n 'Error loading lifecycle rules: ' + error.message +\n '<\\/div>';\n });\n });\n });\n\n // Save the whole working rule set back to the bucket.\n document.getElementById('lifecycleSaveAllBtn').addEventListener('click', function() {\n if (!lifecycleEditor.bucket) return;\n // Fold in any rule the user is still editing so Save doesn't\n // silently discard it; abort if that form isn't valid yet.\n if (!commitOpenLifecycleRuleForm()) return;\n // An empty list clears the configuration, same as Delete all rules,\n // so it gets the same prompt.\n if (lifecycleEditor.rules.length === 0 &&\n !confirm('No rules left: saving removes the lifecycle configuration for ' + lifecycleEditor.bucket + '. This cannot be undone.')) {\n return;\n }\n\n fetch(basePath('/api/s3/buckets/' + lifecycleEditor.bucket + '/lifecycle'), {\n method: 'PUT',\n headers: csrfHeaders(),\n body: JSON.stringify({ rules: lifecycleEditor.rules })\n })\n .then(parseLifecycleResponse)\n .then(({ ok, data }) => {\n if (!ok || data.error) {\n alert('Error saving lifecycle rules: ' + (data.error || 'unknown error'));\n return;\n }\n if (lifecycleModalInstance) {\n lifecycleModalInstance.hide();\n }\n setTimeout(() => location.reload(), 500);\n })\n .catch(error => {\n console.error('Error:', error);\n alert('Error saving lifecycle rules: ' + error.message);\n });\n });\n\n // Clear the lifecycle configuration entirely.\n document.getElementById('lifecycleDeleteAllBtn').addEventListener('click', function() {\n if (!lifecycleEditor.bucket) return;\n if (!confirm('Delete all lifecycle rules for ' + lifecycleEditor.bucket + '? This cannot be undone.')) return;\n\n fetch(basePath('/api/s3/buckets/' + lifecycleEditor.bucket + '/lifecycle'), {\n method: 'DELETE',\n headers: csrfHeaders()\n })\n .then(parseLifecycleResponse)\n .then(({ ok, data }) => {\n if (!ok || data.error) {\n alert('Error deleting lifecycle rules: ' + (data.error || 'unknown error'));\n return;\n }\n if (lifecycleModalInstance) {\n lifecycleModalInstance.hide();\n }\n setTimeout(() => location.reload(), 500);\n })\n .catch(error => {\n console.error('Error:', error);\n alert('Error deleting lifecycle rules: ' + error.message);\n });\n });\n\n // Policy buttons: column badge and action button. The visual editor\n // (weed/admin/static/js/policy_editor.js) is set up once per page\n // load; registerPolicyEditor is re-called on every open so the\n // resource-ARN autocomplete and default Principal stay pinned to\n // whichever bucket is currently open.\n let bucketPolicyEditorSetUp = false;\n document.getElementById('bucketPolicyInsertSampleBtn').addEventListener('click', function() {\n if (!policyEditorBucket) return;\n insertSamplePolicy('bucket', {\n Version: '2012-10-17',\n Statement: [{\n Effect: 'Allow',\n Principal: '*',\n Action: ['s3:GetObject'],\n Resource: ['arn:aws:s3:::' + policyEditorBucket + '/*']\n }]\n });\n });\n\n document.querySelectorAll('.policy-btn').forEach(button => {\n button.addEventListener('click', function() {\n const bucketName = this.dataset.bucketName;\n policyEditorBucket = bucketName;\n bucketPolicyLoaded = false;\n // Deliberately not resetting bucketPolicyMutationInFlight\n // here: if a Save/Delete for a previously open bucket is\n // still in flight, Save/Delete for this bucket must stay\n // blocked until that request settles (its own completion\n // handler releases the flag once done, whether or not it's\n // still the active bucket by then) - otherwise this bucket's\n // mutation could race the abandoned one.\n registerPolicyEditor('bucket', { requirePrincipal: true, bucket: bucketName });\n if (!bucketPolicyEditorSetUp) {\n setupPolicyEditor('bucket');\n bucketPolicyEditorSetUp = true;\n }\n\n document.getElementById('bucketPolicyModalLabel').innerHTML =\n '<i class=\"bi bi-file-earmark-lock me-2\"></i>Bucket Policy - ' + escapeHtml(bucketName);\n document.getElementById('bucketPolicyLoading').style.display = '';\n document.getElementById('bucketPolicyError').style.display = 'none';\n document.getElementById('bucketPolicyEditorWrapper').style.display = 'none';\n\n policyModalInstance.show();\n\n // Drop responses that arrive after another bucket was opened\n const requestSeq = ++policyRequestSeq;\n fetch(basePath('/api/s3/buckets/' + encodeURIComponent(bucketName) + '/policy'))\n .then(parseLifecycleResponse)\n .then(({ ok, data }) => {\n if (requestSeq !== policyRequestSeq) return;\n document.getElementById('bucketPolicyLoading').style.display = 'none';\n if (!ok || data.error) {\n document.getElementById('bucketPolicyError').textContent =\n 'Error loading bucket policy: ' + (data.error || 'unknown error');\n document.getElementById('bucketPolicyError').style.display = '';\n return;\n }\n document.getElementById('bucketPolicyEditorWrapper').style.display = '';\n // policy_text carries stored bytes the server-side\n // decoder rejects; the JSON tab shows them so they\n // can be fixed or deleted.\n document.getElementById('bucketPolicyDocument').value =\n data.policy ? JSON.stringify(data.policy, null, 2) : (data.policy_text || '');\n bucketPolicyLoaded = true;\n loadPolicyTextareaIntoEditor('bucket');\n })\n .catch(error => {\n if (requestSeq !== policyRequestSeq) return;\n console.error('Error fetching bucket policy:', error);\n document.getElementById('bucketPolicyLoading').style.display = 'none';\n document.getElementById('bucketPolicyError').textContent =\n 'Error loading bucket policy: ' + error.message;\n document.getElementById('bucketPolicyError').style.display = '';\n });\n });\n });\n\n // Save the policy document currently shown (whichever tab is active).\n document.getElementById('bucketPolicySaveBtn').addEventListener('click', function() {\n if (!policyEditorBucket) return;\n if (bucketPolicyMutationInFlight) return;\n if (!bucketPolicyLoaded) {\n alert('The current policy has not finished loading. Close and reopen this dialog before saving.');\n return;\n }\n\n // Commit first: if the Editor tab is active, the textarea still\n // holds whatever was there at load time until this runs, so a\n // policy entered purely through the structured form (no tab\n // switch) would otherwise read back as empty here.\n if (!commitPolicyActiveTab('bucket')) return;\n\n const raw = document.getElementById('bucketPolicyDocument').value.trim();\n if (!raw || !policyTextHasStatements(raw)) {\n alert('Add at least one statement, or use Delete to remove the bucket policy.');\n return;\n }\n\n let policy;\n try {\n policy = JSON.parse(document.getElementById('bucketPolicyDocument').value);\n } catch (e) {\n alert('Invalid JSON: ' + e.message);\n return;\n }\n const validationError = validatePolicyEditorDoc('bucket', policy);\n if (validationError) {\n alert(validationError);\n return;\n }\n // The admin API decodes only Version and Statement, so warn\n // before other top-level fields are silently dropped - the IAM\n // policies page already does. (The S3 Tables dialogs don't:\n // their backend stores the document verbatim.)\n if (!confirmPolicyFieldDiscard('bucket')) return;\n\n // Captured now: if the dialog moves on to a different bucket\n // before this PUT resolves, the completion below must not hide\n // or reload over whatever that other bucket is now showing.\n const targetBucket = policyEditorBucket;\n setBucketPolicyMutationInFlight(true);\n fetch(basePath('/api/s3/buckets/' + encodeURIComponent(targetBucket) + '/policy'), {\n method: 'PUT',\n headers: csrfHeaders(),\n body: JSON.stringify({ policy: policy })\n })\n .then(parseLifecycleResponse)\n .then(({ ok, data }) => {\n setBucketPolicyMutationInFlight(false);\n const stillCurrent = targetBucket === policyEditorBucket;\n if (!ok || data.error) {\n if (stillCurrent) {\n alert('Error saving bucket policy: ' + (data.error || 'unknown error'));\n } else {\n console.error('Error saving policy for ' + targetBucket + ' (no longer the open bucket): ' + (data.error || 'unknown error'));\n }\n return;\n }\n if (!stillCurrent) return;\n if (policyModalInstance) {\n policyModalInstance.hide();\n }\n setTimeout(() => location.reload(), 500);\n })\n .catch(error => {\n setBucketPolicyMutationInFlight(false);\n console.error('Error:', error);\n if (targetBucket === policyEditorBucket) {\n alert('Error saving bucket policy: ' + error.message);\n }\n });\n });\n\n // Clear the bucket policy entirely.\n document.getElementById('bucketPolicyDeleteBtn').addEventListener('click', function() {\n if (!policyEditorBucket) return;\n if (bucketPolicyMutationInFlight) return;\n if (!bucketPolicyLoaded) {\n alert('The current policy has not finished loading. Close and reopen this dialog before deleting.');\n return;\n }\n if (!confirm('Delete the bucket policy for ' + policyEditorBucket + '? This cannot be undone.')) return;\n\n // Captured now: if the dialog moves on to a different bucket\n // before this DELETE resolves, the completion below must not\n // hide or reload over whatever that other bucket is now showing.\n const targetBucket = policyEditorBucket;\n setBucketPolicyMutationInFlight(true);\n fetch(basePath('/api/s3/buckets/' + encodeURIComponent(targetBucket) + '/policy'), {\n method: 'DELETE',\n headers: csrfHeaders()\n })\n .then(parseLifecycleResponse)\n .then(({ ok, data }) => {\n setBucketPolicyMutationInFlight(false);\n const stillCurrent = targetBucket === policyEditorBucket;\n if (!ok || data.error) {\n if (stillCurrent) {\n alert('Error deleting bucket policy: ' + (data.error || 'unknown error'));\n } else {\n console.error('Error deleting policy for ' + targetBucket + ' (no longer the open bucket): ' + (data.error || 'unknown error'));\n }\n return;\n }\n if (!stillCurrent) return;\n if (policyModalInstance) {\n policyModalInstance.hide();\n }\n setTimeout(() => location.reload(), 500);\n })\n .catch(error => {\n setBucketPolicyMutationInFlight(false);\n console.error('Error:', error);\n if (targetBucket === policyEditorBucket) {\n alert('Error deleting bucket policy: ' + error.message);\n }\n });\n });\n });\n\n function deleteBucket() {\n const bucketName = document.getElementById('deleteBucketModal').dataset.bucketName;\n if (!bucketName) return;\n\n fetch(basePath(`/api/s3/buckets/${bucketName}`), {\n method: 'DELETE'\n })\n .then(response => response.json())\n .then(data => {\n if (data.error) {\n alert('Error deleting bucket: ' + data.error);\n } else {\n alert('Bucket deleted successfully!');\n // Properly close the modal before reloading\n if (deleteModalInstance) {\n deleteModalInstance.hide();\n }\n setTimeout(() => location.reload(), 500);\n }\n })\n .catch(error => {\n console.error('Error:', error);\n alert('Error deleting bucket: ' + error.message);\n });\n }\n\nfunction displayBucketDetails(data) {\n const bucket = data.bucket;\n\n function formatDate(dateString) {\n const date = new Date(dateString);\n return date.toLocaleString();\n }\n\n let ownerHtml = '<span class=\"text-muted\">No owner (admin-only)</span>';\n if (bucket.owner) {\n ownerHtml = '<span class=\"badge bg-info\"><i class=\"bi bi-person me-1\"></i>' + escapeHtml(bucket.owner) + '</span>';\n }\n\n let usageHtml = '';\n if (bucket.physical_size > 0 && bucket.logical_size > 0 && bucket.physical_size > bucket.logical_size) {\n const overhead = (bucket.physical_size / bucket.logical_size).toFixed(1);\n usageHtml = '<br><small class=\"text-muted\">' + overhead + 'x overhead<\\/small>';\n }\n\n let quotaHtml = '<span class=\"badge bg-secondary\">Disabled</span>';\n if (bucket.quota_enabled) {\n quotaHtml = '<span class=\"badge bg-success\">' + formatBytes(bucket.quota) + '</span>';\n }\n\n let versioningHtml = '<span class=\"text-muted\">Not configured</span>';\n if (bucket.versioning_status === 'Enabled') {\n versioningHtml = '<span class=\"badge bg-success\"><i class=\"bi bi-check me-1\"></i>Enabled</span>';\n } else if (bucket.versioning_status === 'Suspended') {\n versioningHtml = '<span class=\"badge bg-warning\"><i class=\"bi bi-pause me-1\"></i>Suspended</span>';\n }\n\n let statusHtml = '<span class=\"badge bg-success\">Writable</span>';\n if (bucket.read_only) {\n statusHtml = '<span class=\"badge bg-danger\"><i class=\"bi bi-lock me-1\"></i>Read-only</span>';\n }\n\n let objectLockHtml = '<span class=\"text-muted\">Not configured</span>';\n if (bucket.object_lock_enabled) {\n let details = '';\n if (bucket.object_lock_mode && bucket.object_lock_duration > 0) {\n details = '<br><small class=\"text-muted\">' + escapeHtml(bucket.object_lock_mode) + ' • ' + bucket.object_lock_duration + ' days<\\/small>';\n }\n objectLockHtml = '<span class=\"badge bg-warning\"><i class=\"bi bi-lock me-1\"></i>Enabled</span>' + details;\n }\n\n let policyHtml = '<span class=\"text-muted\">Not configured</span>';\n if (bucket.policy_statement_count > 0) {\n const label = bucket.policy_statement_count === 1 ? '1 statement' : bucket.policy_statement_count + ' statements';\n policyHtml = '<span class=\"badge bg-primary\">' + label + '<\\/span>';\n }\n\n const rows = [\n '<div class=\"row\">',\n '<div class=\"col-md-6\">',\n '<h6><i class=\"bi bi-info-circle me-2\"></i>Bucket Information</h6>',\n '<table class=\"table table-sm\">',\n '<tr><td><strong>Name:</strong></td><td>' + escapeHtml(bucket.name) + '<\\/td><\\/tr>',\n '<tr><td><strong>Owner:</strong></td><td>' + ownerHtml + '<\\/td><\\/tr>',\n '<tr><td><strong>Created:</strong></td><td>' + formatDate(bucket.created_at) + '<\\/td><\\/tr>',\n '<tr><td><strong>Last Modified:</strong></td><td>' + formatDate(bucket.last_modified) + '<\\/td><\\/tr>',\n '<tr><td><strong>Logical Size:</strong></td><td>' + formatBytes(bucket.logical_size) + '<\\/td><\\/tr>',\n '<tr><td><strong>Physical Size:</strong></td><td>' + formatBytes(bucket.physical_size) + usageHtml + '<\\/td><\\/tr>',\n '<\\/table>',\n '<\\/div>',\n '<div class=\"col-md-6\">',\n '<h6><i class=\"bi bi-gear-wide-connected me-2\"></i>Configuration</h6>',\n '<table class=\"table table-sm\">',\n '<tr><td><strong>Status:</strong></td><td>' + statusHtml + '<\\/td><\\/tr>',\n '<tr><td><strong>Quota:</strong></td><td>' + quotaHtml + '<\\/td><\\/tr>',\n '<tr><td><strong>Versioning:</strong></td><td>' + versioningHtml + '<\\/td><\\/tr>',\n '<tr><td><strong>Object Lock:</strong></td><td>' + objectLockHtml + '<\\/td><\\/tr>',\n '<tr><td><strong>Policy:</strong></td><td>' + policyHtml + '<\\/td><\\/tr>',\n '<\\/table>',\n '<\\/div>',\n '<\\/div>'\n ];\n\n document.getElementById('bucketDetailsContent').innerHTML = rows.join('');\n}\n\n// displayBucketLifecycle loads the GET response into the local working copy\n// and renders the editor. Nothing is sent back to the server until Save or\n// Delete all rules is clicked.\nfunction displayBucketLifecycle(data) {\n lifecycleEditor.bucket = data.bucket || lifecycleEditor.bucket;\n lifecycleEditor.rules = (data.rules || []).map(cloneLifecycleRule);\n lifecycleEditor.editingIndex = null;\n lifecycleEditor.editingRule = null;\n lifecycleEditor.rawXml = data.xml || '';\n // SeaweedFS doesn't support Transition rules (PutBucketLifecycleConfiguration\n // rejects them for Enabled rules), and the canonical rule shape the admin\n // edits has no field for one, so a Transition set by another client would\n // silently disappear on the next Save. Warn instead.\n // Matches <Transition> and <NoncurrentVersionTransition> however they are\n // spelled: with attributes, self-closed, or namespace-prefixed.\n lifecycleEditor.hasTransition = /<(\\w+:)?(NoncurrentVersion)?Transition[\\s>\\/]/.test(data.xml || '');\n renderLifecycleEditor();\n}\n\n// normalizeLifecycleStatus maps a stored status onto the two the editor can\n// show. The S3 API stores <Status> unvalidated, and the engine runs a rule\n// only on an exact \"Enabled\", so everything else is Disabled - collapsing it\n// here keeps an odd spelling from leaving both radios unchecked, which would\n// throw out of readLifecycleRuleFromForm and make the modal look dead.\nfunction normalizeLifecycleStatus(status) {\n return status === 'Enabled' ? 'Enabled' : 'Disabled';\n}\n\nfunction cloneLifecycleRule(r) {\n return {\n id: r.id || '',\n status: normalizeLifecycleStatus(r.status),\n prefix: r.prefix || '',\n tags: Object.assign({}, r.tags || {}),\n size_greater_than: r.size_greater_than || null,\n size_less_than: r.size_less_than || null,\n expiration_days: r.expiration_days || null,\n expiration_date: r.expiration_date || '',\n expired_object_delete_marker: !!r.expired_object_delete_marker,\n noncurrent_version_expiration_days: r.noncurrent_version_expiration_days || null,\n newer_noncurrent_versions: r.newer_noncurrent_versions || null,\n abort_multipart_days: r.abort_multipart_days || null,\n };\n}\n\nfunction blankLifecycleRule() {\n return {\n id: '', status: 'Enabled', prefix: '', tags: {},\n size_greater_than: null, size_less_than: null,\n expiration_days: null, expiration_date: '',\n expired_object_delete_marker: false,\n noncurrent_version_expiration_days: null, newer_noncurrent_versions: null,\n abort_multipart_days: null,\n };\n}\n\nfunction describeLifecycleScope(rule) {\n const parts = [];\n if (rule.prefix) {\n parts.push('<code>' + escapeHtml(rule.prefix) + '<\\/code>');\n }\n Object.entries(rule.tags || {}).forEach(([k, v]) => {\n parts.push('<span class=\"badge bg-light text-dark border\">' + escapeHtml(k) + '=' + escapeHtml(v) + '<\\/span>');\n });\n if (rule.size_greater_than) {\n parts.push('size > ' + formatBytes(rule.size_greater_than));\n }\n if (rule.size_less_than) {\n parts.push('size < ' + formatBytes(rule.size_less_than));\n }\n return parts.length ? parts.join(' ') : '<span class=\"text-muted\">Whole bucket<\\/span>';\n}\n\nfunction describeLifecycleActions(rule) {\n const actions = [];\n if (rule.expiration_days) {\n actions.push('Expire after ' + formatDays(rule.expiration_days));\n }\n if (rule.expiration_date) {\n actions.push('Expire on ' + escapeHtml(rule.expiration_date));\n }\n if (rule.expired_object_delete_marker) {\n actions.push('Remove expired object delete markers');\n }\n if (rule.noncurrent_version_expiration_days) {\n let action = 'Expire noncurrent versions after ' + formatDays(rule.noncurrent_version_expiration_days);\n if (rule.newer_noncurrent_versions) {\n action += ', keep ' + rule.newer_noncurrent_versions + ' newest';\n }\n actions.push(action);\n } else if (rule.newer_noncurrent_versions) {\n // Standalone (no expiration_days): the engine still recognizes this\n // as an action that expires noncurrent versions beyond the N newest.\n actions.push('Keep ' + rule.newer_noncurrent_versions + ' newest noncurrent versions');\n }\n if (rule.abort_multipart_days) {\n actions.push('Abort incomplete multipart uploads after ' + formatDays(rule.abort_multipart_days));\n }\n if (actions.length === 0) {\n return '<span class=\"text-muted\">None<\\/span>';\n }\n return actions.join('<br>');\n}\n\nfunction lifecycleRuleRowHtml(rule, idx) {\n const statusHtml = rule.status === 'Enabled'\n ? '<span class=\"badge bg-success\">Enabled<\\/span>'\n : '<span class=\"badge bg-secondary\">' + escapeHtml(rule.status) + '<\\/span>';\n return '<tr>' +\n '<td>' + (rule.id ? escapeHtml(rule.id) : '<span class=\"text-muted\">unnamed<\\/span>') + '<\\/td>' +\n '<td>' + statusHtml + '<\\/td>' +\n '<td>' + describeLifecycleScope(rule) + '<\\/td>' +\n '<td>' + describeLifecycleActions(rule) + '<\\/td>' +\n '<td class=\"text-end text-nowrap\">' +\n '<button type=\"button\" class=\"btn btn-sm btn-outline-secondary lifecycle-edit-rule-btn\" data-index=\"' + idx + '\" title=\"Edit rule\"><i class=\"bi bi-pen\"><\\/i><\\/button> ' +\n '<button type=\"button\" class=\"btn btn-sm btn-outline-danger lifecycle-delete-rule-btn\" data-index=\"' + idx + '\" title=\"Delete rule\"><i class=\"bi bi-trash\"><\\/i><\\/button>' +\n '<\\/td>' +\n '<\\/tr>';\n}\n\nfunction lifecycleRuleFormHtml() {\n const rule = lifecycleEditor.editingRule;\n const tagRowHtml = (k, v) =>\n '<div class=\"input-group input-group-sm mb-1 lifecycle-tag-row\">' +\n '<input type=\"text\" class=\"form-control lifecycle-tag-key\" placeholder=\"Key\" value=\"' + escapeHtml(k) + '\">' +\n '<input type=\"text\" class=\"form-control lifecycle-tag-value\" placeholder=\"Value\" value=\"' + escapeHtml(v) + '\">' +\n '<button type=\"button\" class=\"btn btn-outline-danger lifecycle-remove-tag-btn\"><i class=\"bi bi-x\"><\\/i><\\/button>' +\n '<\\/div>';\n const tagRows = Object.entries(rule.tags || {}).map(([k, v]) => tagRowHtml(k, v)).join('');\n\n return '' +\n '<div class=\"card mb-3\" id=\"lifecycleRuleForm\">' +\n '<div class=\"card-body\">' +\n '<h6 class=\"card-title\">' + (lifecycleEditor.editingIndex === 'new' ? 'Add rule' : 'Edit rule') + '<\\/h6>' +\n\n '<div class=\"row g-2 mb-2\">' +\n '<div class=\"col-md-6\">' +\n '<label class=\"form-label small\">ID (optional)<\\/label>' +\n '<input type=\"text\" class=\"form-control form-control-sm\" id=\"lifecycleRuleId\" value=\"' + escapeHtml(rule.id) + '\">' +\n '<\\/div>' +\n '<div class=\"col-md-6\">' +\n '<label class=\"form-label small d-block\">Status<\\/label>' +\n '<div class=\"form-check form-check-inline\">' +\n '<input class=\"form-check-input\" type=\"radio\" name=\"lifecycleRuleStatus\" id=\"lifecycleStatusEnabled\" value=\"Enabled\"' + (rule.status === 'Enabled' ? ' checked' : '') + '>' +\n '<label class=\"form-check-label\" for=\"lifecycleStatusEnabled\">Enabled<\\/label>' +\n '<\\/div>' +\n '<div class=\"form-check form-check-inline\">' +\n '<input class=\"form-check-input\" type=\"radio\" name=\"lifecycleRuleStatus\" id=\"lifecycleStatusDisabled\" value=\"Disabled\"' + (rule.status === 'Disabled' ? ' checked' : '') + '>' +\n '<label class=\"form-check-label\" for=\"lifecycleStatusDisabled\">Disabled<\\/label>' +\n '<\\/div>' +\n '<\\/div>' +\n '<\\/div>' +\n\n '<div class=\"mb-2\">' +\n '<label class=\"form-label small\">Prefix<\\/label>' +\n '<input type=\"text\" class=\"form-control form-control-sm\" id=\"lifecycleRulePrefix\" placeholder=\"logs/\" value=\"' + escapeHtml(rule.prefix) + '\">' +\n '<\\/div>' +\n\n '<div class=\"mb-2\">' +\n '<label class=\"form-label small d-block\">Tags<\\/label>' +\n '<div id=\"lifecycleTagRows\">' + tagRows + '<\\/div>' +\n '<button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" id=\"lifecycleAddTagBtn\"><i class=\"bi bi-plus me-1\"><\\/i>Add tag<\\/button>' +\n '<\\/div>' +\n\n '<div class=\"row g-2 mb-3\">' +\n '<div class=\"col-md-6\">' +\n '<label class=\"form-label small\">Size greater than (bytes)<\\/label>' +\n '<input type=\"number\" class=\"form-control form-control-sm\" id=\"lifecycleRuleSizeGT\" min=\"0\" step=\"1\" value=\"' + (rule.size_greater_than || '') + '\">' +\n '<\\/div>' +\n '<div class=\"col-md-6\">' +\n '<label class=\"form-label small\">Size less than (bytes)<\\/label>' +\n '<input type=\"number\" class=\"form-control form-control-sm\" id=\"lifecycleRuleSizeLT\" min=\"0\" step=\"1\" value=\"' + (rule.size_less_than || '') + '\">' +\n '<\\/div>' +\n '<\\/div>' +\n\n '<hr>' +\n '<h6 class=\"small text-muted text-uppercase\">Actions<\\/h6>' +\n\n '<div class=\"row g-2 align-items-center mb-2\">' +\n '<div class=\"col-auto\">' +\n '<div class=\"form-check\">' +\n '<input class=\"form-check-input\" type=\"checkbox\" id=\"lifecycleExpireDaysEnabled\"' + (rule.expiration_days ? ' checked' : '') + '>' +\n '<label class=\"form-check-label\" for=\"lifecycleExpireDaysEnabled\">Expire after<\\/label>' +\n '<\\/div>' +\n '<\\/div>' +\n '<div class=\"col-auto\">' +\n '<input type=\"number\" class=\"form-control form-control-sm\" id=\"lifecycleExpireDays\" min=\"1\" step=\"1\" style=\"width: 6rem;\" value=\"' + (rule.expiration_days || '') + '\"' + (rule.expiration_days ? '' : ' disabled') + '>' +\n '<\\/div><div class=\"col-auto\">days<\\/div>' +\n '<\\/div>' +\n\n '<div class=\"row g-2 align-items-center mb-2\">' +\n '<div class=\"col-auto\">' +\n '<div class=\"form-check\">' +\n '<input class=\"form-check-input\" type=\"checkbox\" id=\"lifecycleExpireDateEnabled\"' + (rule.expiration_date ? ' checked' : '') + '>' +\n '<label class=\"form-check-label\" for=\"lifecycleExpireDateEnabled\">Expire on date<\\/label>' +\n '<\\/div>' +\n '<\\/div>' +\n '<div class=\"col-auto\">' +\n '<input type=\"date\" class=\"form-control form-control-sm\" id=\"lifecycleExpireDate\" value=\"' + escapeHtml(rule.expiration_date) + '\"' + (rule.expiration_date ? '' : ' disabled') + '>' +\n '<\\/div>' +\n '<\\/div>' +\n\n '<div class=\"form-check mb-2\">' +\n '<input class=\"form-check-input\" type=\"checkbox\" id=\"lifecycleDeleteMarker\"' + (rule.expired_object_delete_marker ? ' checked' : '') + '>' +\n '<label class=\"form-check-label\" for=\"lifecycleDeleteMarker\">Remove expired object delete markers<\\/label>' +\n '<\\/div>' +\n\n (() => {\n const noncurrentActive = !!(rule.noncurrent_version_expiration_days || rule.newer_noncurrent_versions);\n return '<div class=\"row g-2 align-items-center mb-2\">' +\n '<div class=\"col-auto\">' +\n '<div class=\"form-check\">' +\n '<input class=\"form-check-input\" type=\"checkbox\" id=\"lifecycleNoncurrentEnabled\"' + (noncurrentActive ? ' checked' : '') + '>' +\n '<label class=\"form-check-label\" for=\"lifecycleNoncurrentEnabled\">Limit noncurrent versions<\\/label>' +\n '<\\/div>' +\n '<\\/div>' +\n '<div class=\"col-auto\">after<\\/div>' +\n '<div class=\"col-auto\">' +\n '<input type=\"number\" class=\"form-control form-control-sm\" id=\"lifecycleNoncurrentDays\" min=\"1\" step=\"1\" style=\"width: 6rem;\" value=\"' + (rule.noncurrent_version_expiration_days || '') + '\"' + (noncurrentActive ? '' : ' disabled') + '>' +\n '<\\/div><div class=\"col-auto\">days (optional), keep<\\/div>' +\n '<div class=\"col-auto\">' +\n '<input type=\"number\" class=\"form-control form-control-sm\" id=\"lifecycleNoncurrentKeep\" min=\"0\" step=\"1\" style=\"width: 6rem;\" value=\"' + (rule.newer_noncurrent_versions || '') + '\"' + (noncurrentActive ? '' : ' disabled') + '>' +\n '<\\/div><div class=\"col-auto\">newest (optional)<\\/div>' +\n '<\\/div>';\n })() +\n\n '<div class=\"row g-2 align-items-center mb-3\">' +\n '<div class=\"col-auto\">' +\n '<div class=\"form-check\">' +\n '<input class=\"form-check-input\" type=\"checkbox\" id=\"lifecycleAbortMPUEnabled\"' + (rule.abort_multipart_days ? ' checked' : '') + '>' +\n '<label class=\"form-check-label\" for=\"lifecycleAbortMPUEnabled\">Abort incomplete multipart uploads after<\\/label>' +\n '<\\/div>' +\n '<\\/div>' +\n '<div class=\"col-auto\">' +\n '<input type=\"number\" class=\"form-control form-control-sm\" id=\"lifecycleAbortMPUDays\" min=\"1\" step=\"1\" style=\"width: 6rem;\" value=\"' + (rule.abort_multipart_days || '') + '\"' + (rule.abort_multipart_days ? '' : ' disabled') + '>' +\n '<\\/div><div class=\"col-auto\">days<\\/div>' +\n '<\\/div>' +\n\n '<div class=\"text-end\">' +\n '<button type=\"button\" class=\"btn btn-sm btn-secondary\" id=\"lifecycleCancelRuleBtn\">Cancel<\\/button> ' +\n '<button type=\"button\" class=\"btn btn-sm btn-primary\" id=\"lifecycleSaveRuleBtn\">Save rule<\\/button>' +\n '<\\/div>' +\n '<\\/div>' +\n '<\\/div>';\n}\n\nfunction renderLifecycleEditor() {\n const container = document.getElementById('bucketLifecycleContent');\n const rowsHtml = lifecycleEditor.rules.map(lifecycleRuleRowHtml).join('') ||\n '<tr><td colspan=\"5\" class=\"text-center text-muted py-3\">No lifecycle rules<\\/td><\\/tr>';\n\n let html = '';\n if (lifecycleEditor.hasTransition) {\n html += '<div class=\"alert alert-warning small\">' +\n '<i class=\"bi bi-exclamation-triangle me-1\"><\\/i>' +\n 'This bucket has a Transition rule set outside the admin (SeaweedFS does not support storage class transitions). Saving from here will drop it.' +\n '<\\/div>';\n }\n\n html += '<div class=\"table-responsive mb-3\">' +\n '<table class=\"table table-sm align-middle\">' +\n '<thead><tr><th>Rule<\\/th><th>Status<\\/th><th>Scope<\\/th><th>Actions<\\/th><th><\\/th><\\/tr><\\/thead>' +\n '<tbody>' + rowsHtml + '<\\/tbody>' +\n '<\\/table>' +\n '<\\/div>' +\n '<button type=\"button\" class=\"btn btn-sm btn-outline-primary mb-3\" id=\"lifecycleAddRuleBtn\">' +\n '<i class=\"bi bi-plus me-1\"><\\/i>Add rule' +\n '<\\/button>';\n\n if (lifecycleEditor.editingIndex !== null) {\n html += lifecycleRuleFormHtml();\n }\n\n if (lifecycleEditor.rawXml) {\n html += '<details class=\"mt-2\">' +\n '<summary class=\"text-muted small\">Raw XML (as currently stored)<\\/summary>' +\n '<pre class=\"bg-light border rounded p-2 mt-2 small mb-0\">' + escapeHtml(lifecycleEditor.rawXml) + '<\\/pre>' +\n '<\\/details>';\n }\n\n container.innerHTML = html;\n bindLifecycleEditorEvents();\n}\n\nfunction bindLifecycleToggle(checkboxId, fieldId, mutuallyExclusiveId) {\n const checkbox = document.getElementById(checkboxId);\n const field = document.getElementById(fieldId);\n if (!checkbox || !field) return;\n checkbox.addEventListener('change', function() {\n field.disabled = !this.checked;\n if (this.checked && mutuallyExclusiveId) {\n const other = document.getElementById(mutuallyExclusiveId);\n if (other && other.checked) {\n other.checked = false;\n other.dispatchEvent(new Event('change'));\n }\n }\n });\n}\n\nfunction bindLifecycleEditorEvents() {\n const addBtn = document.getElementById('lifecycleAddRuleBtn');\n if (addBtn) {\n addBtn.addEventListener('click', function() {\n // Fold in any rule already being edited before switching forms,\n // so an invalid in-progress draft isn't silently discarded.\n if (!commitOpenLifecycleRuleForm()) return;\n lifecycleEditor.editingIndex = 'new';\n lifecycleEditor.editingRule = blankLifecycleRule();\n renderLifecycleEditor();\n });\n }\n\n document.querySelectorAll('.lifecycle-edit-rule-btn').forEach(btn => {\n btn.addEventListener('click', function() {\n const idx = parseInt(this.dataset.index, 10);\n // Same as above: committing never shifts indices (it either\n // pushes a new rule at the end or replaces one in place), so\n // idx captured above is still valid after this call.\n if (!commitOpenLifecycleRuleForm()) return;\n lifecycleEditor.editingIndex = idx;\n lifecycleEditor.editingRule = cloneLifecycleRule(lifecycleEditor.rules[idx]);\n renderLifecycleEditor();\n });\n });\n\n document.querySelectorAll('.lifecycle-delete-rule-btn').forEach(btn => {\n btn.addEventListener('click', function() {\n const idx = parseInt(this.dataset.index, 10);\n // Fold in an open form first, like every other transition does:\n // re-rendering after the splice would otherwise redraw it from the\n // snapshot taken when editing began and lose what was typed.\n if (lifecycleEditor.editingIndex !== null && lifecycleEditor.editingIndex !== idx) {\n if (!commitOpenLifecycleRuleForm()) return;\n }\n lifecycleEditor.rules.splice(idx, 1);\n // Deleting a rule shifts every later index down by one, so an\n // open edit form must follow its rule rather than silently\n // start targeting whatever now sits at its old index.\n if (lifecycleEditor.editingIndex === idx) {\n lifecycleEditor.editingIndex = null;\n lifecycleEditor.editingRule = null;\n } else if (typeof lifecycleEditor.editingIndex === 'number' && lifecycleEditor.editingIndex > idx) {\n lifecycleEditor.editingIndex -= 1;\n }\n renderLifecycleEditor();\n });\n });\n\n const cancelBtn = document.getElementById('lifecycleCancelRuleBtn');\n if (cancelBtn) {\n cancelBtn.addEventListener('click', function() {\n lifecycleEditor.editingIndex = null;\n lifecycleEditor.editingRule = null;\n renderLifecycleEditor();\n });\n }\n\n const addTagBtn = document.getElementById('lifecycleAddTagBtn');\n if (addTagBtn) {\n addTagBtn.addEventListener('click', function() {\n const rows = document.getElementById('lifecycleTagRows');\n const row = document.createElement('div');\n row.className = 'input-group input-group-sm mb-1 lifecycle-tag-row';\n row.innerHTML =\n '<input type=\"text\" class=\"form-control lifecycle-tag-key\" placeholder=\"Key\">' +\n '<input type=\"text\" class=\"form-control lifecycle-tag-value\" placeholder=\"Value\">' +\n '<button type=\"button\" class=\"btn btn-outline-danger lifecycle-remove-tag-btn\"><i class=\"bi bi-x\"><\\/i><\\/button>';\n rows.appendChild(row);\n row.querySelector('.lifecycle-remove-tag-btn').addEventListener('click', function() {\n row.remove();\n });\n });\n }\n\n document.querySelectorAll('.lifecycle-remove-tag-btn').forEach(btn => {\n btn.addEventListener('click', function() {\n this.closest('.lifecycle-tag-row').remove();\n });\n });\n\n bindLifecycleToggle('lifecycleExpireDaysEnabled', 'lifecycleExpireDays', 'lifecycleExpireDateEnabled');\n bindLifecycleToggle('lifecycleExpireDateEnabled', 'lifecycleExpireDate', 'lifecycleExpireDaysEnabled');\n bindLifecycleToggle('lifecycleNoncurrentEnabled', 'lifecycleNoncurrentDays');\n bindLifecycleToggle('lifecycleNoncurrentEnabled', 'lifecycleNoncurrentKeep');\n bindLifecycleToggle('lifecycleAbortMPUEnabled', 'lifecycleAbortMPUDays');\n\n const saveRuleBtn = document.getElementById('lifecycleSaveRuleBtn');\n if (saveRuleBtn) {\n saveRuleBtn.addEventListener('click', saveLifecycleRuleFromForm);\n }\n}\n\n// readLifecycleRuleFromForm builds and validates a rule from the currently\n// open edit form's DOM inputs. Returns { rule: null, error: '...' } when the\n// form doesn't hold a valid rule, without mutating any state.\nfunction readLifecycleRuleFromForm() {\n const id = document.getElementById('lifecycleRuleId').value.trim();\n const status = document.querySelector('input[name=\"lifecycleRuleStatus\"]:checked').value;\n const prefix = document.getElementById('lifecycleRulePrefix').value.trim();\n\n const tags = {};\n document.querySelectorAll('#lifecycleTagRows .lifecycle-tag-row').forEach(row => {\n const key = row.querySelector('.lifecycle-tag-key').value.trim();\n const value = row.querySelector('.lifecycle-tag-value').value.trim();\n if (key) {\n tags[key] = value;\n }\n });\n\n const sizeGT = parseInt(document.getElementById('lifecycleRuleSizeGT').value, 10) || 0;\n const sizeLT = parseInt(document.getElementById('lifecycleRuleSizeLT').value, 10) || 0;\n\n const expireDaysEnabled = document.getElementById('lifecycleExpireDaysEnabled').checked;\n const expireDateEnabled = document.getElementById('lifecycleExpireDateEnabled').checked;\n const expirationDays = expireDaysEnabled ? (parseInt(document.getElementById('lifecycleExpireDays').value, 10) || 0) : 0;\n const expirationDate = expireDateEnabled ? document.getElementById('lifecycleExpireDate').value : '';\n\n const deleteMarker = document.getElementById('lifecycleDeleteMarker').checked;\n\n const noncurrentEnabled = document.getElementById('lifecycleNoncurrentEnabled').checked;\n const noncurrentDays = noncurrentEnabled ? (parseInt(document.getElementById('lifecycleNoncurrentDays').value, 10) || 0) : 0;\n const noncurrentKeep = noncurrentEnabled ? (parseInt(document.getElementById('lifecycleNoncurrentKeep').value, 10) || 0) : 0;\n\n const abortEnabled = document.getElementById('lifecycleAbortMPUEnabled').checked;\n const abortDays = abortEnabled ? (parseInt(document.getElementById('lifecycleAbortMPUDays').value, 10) || 0) : 0;\n\n if (expireDaysEnabled && expirationDays <= 0) {\n return { rule: null, error: 'Enter a valid number of days for expiration.' };\n }\n if (expireDateEnabled && !expirationDate) {\n return { rule: null, error: 'Enter a valid expiration date.' };\n }\n if (noncurrentEnabled && noncurrentDays <= 0 && noncurrentKeep <= 0) {\n return { rule: null, error: 'Enter a number of days and/or a newest-versions-to-keep count for noncurrent version handling.' };\n }\n if (abortEnabled && abortDays <= 0) {\n return { rule: null, error: 'Enter a valid number of days for aborting incomplete multipart uploads.' };\n }\n if (!expireDaysEnabled && !expireDateEnabled && !deleteMarker && !noncurrentEnabled && !abortEnabled) {\n return { rule: null, error: 'Select at least one action for this rule.' };\n }\n // The server rejects this combination too; catching it here names the two\n // fields instead of surfacing a 400 after the whole set is submitted.\n if (deleteMarker && (expireDaysEnabled || expireDateEnabled)) {\n return { rule: null, error: 'Removing expired object delete markers cannot be combined with an expiration.' };\n }\n if (sizeGT > 0 && sizeLT > 0 && sizeGT >= sizeLT) {\n return { rule: null, error: '\"Size greater than\" must be less than \"size less than\".' };\n }\n\n return {\n rule: {\n id: id,\n status: status,\n prefix: prefix,\n tags: tags,\n size_greater_than: sizeGT > 0 ? sizeGT : null,\n size_less_than: sizeLT > 0 ? sizeLT : null,\n expiration_days: expirationDays > 0 ? expirationDays : null,\n expiration_date: expirationDate || '',\n expired_object_delete_marker: deleteMarker,\n noncurrent_version_expiration_days: noncurrentDays > 0 ? noncurrentDays : null,\n newer_noncurrent_versions: noncurrentKeep > 0 ? noncurrentKeep : null,\n abort_multipart_days: abortDays > 0 ? abortDays : null,\n },\n error: null,\n };\n}\n\nfunction saveLifecycleRuleFromForm() {\n const { rule, error } = readLifecycleRuleFromForm();\n if (error) {\n alert(error);\n return;\n }\n\n if (lifecycleEditor.editingIndex === 'new') {\n lifecycleEditor.rules.push(rule);\n } else {\n lifecycleEditor.rules[lifecycleEditor.editingIndex] = rule;\n }\n lifecycleEditor.editingIndex = null;\n lifecycleEditor.editingRule = null;\n renderLifecycleEditor();\n}\n\n// commitOpenLifecycleRuleForm folds an in-progress add/edit form into\n// lifecycleEditor.rules before the whole rule set is sent to the server, so\n// clicking the modal's \"Save\" without first clicking \"Save rule\" doesn't\n// silently drop the open edit. Returns false (and leaves the form open) when\n// the open form doesn't hold a valid rule, so the caller can abort the save.\nfunction commitOpenLifecycleRuleForm() {\n if (lifecycleEditor.editingIndex === null) {\n return true;\n }\n\n const { rule, error } = readLifecycleRuleFromForm();\n if (error) {\n alert(error);\n return false;\n }\n\n if (lifecycleEditor.editingIndex === 'new') {\n lifecycleEditor.rules.push(rule);\n } else {\n lifecycleEditor.rules[lifecycleEditor.editingIndex] = rule;\n }\n lifecycleEditor.editingIndex = null;\n lifecycleEditor.editingRule = null;\n renderLifecycleEditor();\n return true;\n}\n\n function goToPage(page) {\n const url = new URL(window.location);\n url.searchParams.set('page', page);\n window.location.href = url.toString();\n }\n\n function changePageSize() {\n const pageSize = document.getElementById('pageSizeSelect').value;\n const url = new URL(window.location);\n url.searchParams.set('pageSize', pageSize);\n url.searchParams.set('page', '1');\n window.location.href = url.toString();\n }\n\n function sortTable(column) {\n const url = new URL(window.location);\n const currentSort = url.searchParams.get('sortBy');\n const currentOrder = url.searchParams.get('sortOrder') || 'asc';\n\n let newOrder = 'asc';\n if (currentSort === column && currentOrder === 'asc') {\n newOrder = 'desc';\n }\n\n url.searchParams.set('sortBy', column);\n url.searchParams.set('sortOrder', newOrder);\n url.searchParams.set('page', '1');\n window.location.href = url.toString();\n }\n\n function exportBucketList() {\n // RFC 4180 compliant CSV escaping: escape double quotes by doubling them\n function escapeCsvField(value) {\n const str = String(value ?? '');\n if (str.includes(',') || str.includes('\"') || str.includes('\\n') || str.includes('\\r')) {\n return '\"' + str.replace(/\"/g, '\"\"') + '\"';\n }\n return '\"' + str + '\"';\n }\n\n // Fetch all buckets from the API (not just the current page)\n fetch(basePath('/api/s3/buckets'))\n .then(response => response.json())\n .then(data => {\n if (data.error) {\n alert('Error exporting buckets: ' + data.error);\n return;\n }\n\n const buckets = data.buckets || [];\n const csvContent = \"data:text/csv;charset=utf-8,\" +\n \"Name,Owner,Logical Size,Physical Size,Created,Quota,Versioning,Object Lock\\n\" +\n buckets.map(b => [\n escapeCsvField(b.name),\n escapeCsvField(b.owner),\n escapeCsvField(formatBytes(b.logical_size)),\n escapeCsvField(formatBytes(b.physical_size)),\n escapeCsvField(b.created_at),\n escapeCsvField(b.quota_enabled ? formatBytes(b.quota) : 'No quota'),\n escapeCsvField(b.versioning_status || 'Not configured'),\n escapeCsvField(b.object_lock_enabled ? 'Enabled' : 'Not configured')\n ].join(',')).join(\"\\n\");\n\n const encodedUri = encodeURI(csvContent);\n const link = document.createElement(\"a\");\n link.setAttribute(\"href\", encodedUri);\n link.setAttribute(\"download\", \"buckets.csv\");\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n })\n .catch(error => {\n console.error('Error:', error);\n alert('Error exporting buckets: ' + error.message);\n });\n }\n </script>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
var _ = templruntime.GeneratedTemplate
|