Files
seaweedFS/weed/admin/view/app/s3tables_buckets_templ.go
Chris Lu 79722bcf30 Add s3tables shell and admin UI (#8172)
* Add shared s3tables manager

* Add s3tables shell commands

* Add s3tables admin API

* Add s3tables admin UI

* Fix admin s3tables namespace create

* Rename table buckets menu

* Centralize s3tables tag validation

* Reuse s3tables manager in admin

* Extract s3tables list limit

* Add s3tables bucket ARN helper

* Remove write middleware from s3tables APIs

* Fix bucket link and policy hint

* Fix table tag parsing and nav link

* Disable namespace table link on invalid ARN

* Improve s3tables error decode

* Return flag parse errors for s3tables tag

* Accept query params for namespace create

* Bind namespace create form data

* Read s3tables JS data from DOM

* s3tables: allow empty region ARN

* shell: pass s3tables account id

* shell: require account for table buckets

* shell: use bucket name for namespaces

* shell: use bucket name for tables

* shell: use bucket name for tags

* admin: add table buckets links in file browser

* s3api: reuse s3tables tag validation

* admin: harden s3tables UI handlers

* fix admin list table buckets

* allow admin s3tables access

* validate s3tables bucket tags

* log s3tables bucket metadata errors

* rollback table bucket on owner failure

* show s3tables bucket owner

* add s3tables iam conditions

* Add s3tables user permissions UI

* Authorize s3tables using identity actions

* Add s3tables permissions to user modal

* Disambiguate bucket scope in user permissions

* Block table bucket names that match S3 buckets

* Pretty-print IAM identity JSON

* Include tags in s3tables permission context

* admin: refactor S3 Tables inline JavaScript into a separate file

* s3tables: extend IAM policy condition operators support

* shell: use LookupEntry wrapper for s3tables bucket conflict check

* admin: handle buildBucketPermissions validation in create/update flows
2026-01-30 22:57:05 -08:00

223 lines
28 KiB
Go

// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.960
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"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
)
func S3TablesBuckets(data dash.S3TablesBucketsData) 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=\"fas fa-table me-2\"></i>S3 Tables Buckets</h1><div class=\"btn-toolbar mb-2 mb-md-0\"><div class=\"btn-group me-2\"><button type=\"button\" class=\"btn btn-sm btn-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#createS3TablesBucketModal\"><i class=\"fas fa-plus me-1\"></i>Create Bucket</button></div></div></div><div id=\"s3tables-buckets-content\"><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/s3tables_buckets.templ`, Line: 34, Col: 47}
}
_, 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, 2, "</div></div><div class=\"col-auto\"><i class=\"fas fa-table fa-2x text-gray-300\"></i></div></div></div></div></div><div class=\"col-xl-4 col-md-6 mb-4\"><div class=\"card border-left-info shadow h-100 py-2\"><div class=\"card-body\"><div class=\"row no-gutters align-items-center\"><div class=\"col mr-2\"><div class=\"text-xs font-weight-bold text-info text-uppercase mb-1\">Last Updated</div><div class=\"h6 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(data.LastUpdated.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 53, Col: 43}
}
_, 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, 3, "</div></div><div class=\"col-auto\"><i class=\"fas fa-clock fa-2x text-gray-300\"></i></div></div></div></div></div></div><div class=\"row\"><div class=\"col-12\"><div class=\"card shadow mb-4\"><div class=\"card-header py-3 d-flex flex-row align-items-center justify-content-between\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-table me-2\"></i>Table Buckets</h6></div><div class=\"card-body\"><div class=\"table-responsive\"><table class=\"table table-hover\" width=\"100%\" cellspacing=\"0\" id=\"s3tablesBucketsTable\"><thead><tr><th>Name</th><th>Owner</th><th>ARN</th><th>Created</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, 4, "<tr><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 87, Col: 28}
}
_, 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, 5, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.OwnerAccountID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 88, Col: 38}
}
_, 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, 6, "</td><td class=\"text-muted small\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.ARN)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 89, Col: 52}
}
_, 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, 7, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, 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/s3tables_buckets.templ`, Line: 90, Col: 60}
}
_, 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, 8, "</td><td><div class=\"btn-group btn-group-sm\" role=\"group\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
bucketName, parseErr := s3tables.ParseBucketNameFromARN(bucket.ARN)
if parseErr == nil {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a class=\"btn btn-outline-primary btn-sm\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 templ.SafeURL
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/object-store/s3tables/buckets/%s/namespaces", bucketName)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 95, Col: 149}
}
_, 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, 10, "\"><i class=\"fas fa-folder-open\"></i></a> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<button type=\"button\" class=\"btn btn-outline-primary btn-sm\" disabled title=\"Invalid bucket ARN\"><i class=\"fas fa-folder-open\"></i></button> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<button type=\"button\" class=\"btn btn-outline-success btn-sm s3tables-tags-btn\" data-resource-arn=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.ARN)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 103, Col: 122}
}
_, 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, 13, "\" title=\"Tags\"><i class=\"fas fa-tags\"></i></button> <button type=\"button\" class=\"btn btn-outline-info btn-sm s3tables-bucket-policy-btn\" data-bucket-arn=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.ARN)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 106, Col: 126}
}
_, 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, 14, "\" title=\"Bucket Policy\"><i class=\"fas fa-shield-alt\"></i></button> <button type=\"button\" class=\"btn btn-outline-danger btn-sm s3tables-delete-bucket-btn\" data-bucket-arn=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.ARN)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 109, Col: 128}
}
_, 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, 15, "\" data-bucket-name=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 109, Col: 161}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" title=\"Delete\"><i class=\"fas fa-trash\"></i></button></div></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(data.Buckets) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<tr><td colspan=\"5\" class=\"text-center text-muted py-4\"><i class=\"fas fa-table fa-3x mb-3 text-muted\"></i><div><h5>No table buckets found</h5><p>Create your first S3 Tables bucket to get started.</p><button type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#createS3TablesBucketModal\"><i class=\"fas fa-plus me-1\"></i>Create Bucket</button></div></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</tbody></table></div></div></div></div></div></div><div class=\"modal fade\" id=\"createS3TablesBucketModal\" tabindex=\"-1\" aria-labelledby=\"createS3TablesBucketModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"createS3TablesBucketModalLabel\"><i class=\"fas fa-plus me-2\"></i>Create Table Bucket</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><form id=\"createS3TablesBucketForm\"><div class=\"modal-body\"><div class=\"mb-3\"><label for=\"s3tablesBucketName\" class=\"form-label\">Bucket Name</label> <input type=\"text\" class=\"form-control\" id=\"s3tablesBucketName\" name=\"name\" placeholder=\"table-bucket-name\" required></div><div class=\"mb-3\"><label for=\"s3tablesBucketOwner\" class=\"form-label\">Owner (Optional)</label> <select class=\"form-select\" id=\"s3tablesBucketOwner\" name=\"owner\"><option value=\"\">No owner (admin-only access)</option></select><div class=\"form-text\">The S3 identity that owns this table bucket. Non-admin users can only access table buckets they own.</div></div><div class=\"mb-3\"><label for=\"s3tablesBucketTags\" class=\"form-label\">Tags</label> <input type=\"text\" class=\"form-control\" id=\"s3tablesBucketTags\" name=\"tags\" placeholder=\"key1=value1,key2=value2\"><div class=\"form-text\">Optional tags in key=value format.</div></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\"><i class=\"fas fa-plus me-1\"></i>Create</button></div></form></div></div></div><div class=\"modal fade\" id=\"deleteS3TablesBucketModal\" tabindex=\"-1\" aria-labelledby=\"deleteS3TablesBucketModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"deleteS3TablesBucketModalLabel\"><i class=\"fas fa-exclamation-triangle me-2 text-warning\"></i>Delete Table 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 table bucket <strong id=\"deleteS3TablesBucketName\"></strong>?</p></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-danger\" onclick=\"deleteS3TablesBucket()\"><i class=\"fas fa-trash me-1\"></i>Delete</button></div></div></div></div><div class=\"modal fade\" id=\"s3tablesBucketPolicyModal\" tabindex=\"-1\" aria-labelledby=\"s3tablesBucketPolicyModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog modal-lg\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"s3tablesBucketPolicyModalLabel\"><i class=\"fas fa-shield-alt me-2\"></i>Table Bucket Policy</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><form id=\"s3tablesBucketPolicyForm\"><div class=\"modal-body\"><input type=\"hidden\" id=\"s3tablesBucketPolicyArn\" name=\"bucket_arn\"><div class=\"mb-3\"><label for=\"s3tablesBucketPolicyText\" class=\"form-label\">Policy JSON</label> <textarea class=\"form-control\" id=\"s3tablesBucketPolicyText\" name=\"policy\" rows=\"12\" placeholder=\"{ }\"></textarea></div><div class=\"form-text\">Provide a policy JSON; use Delete Policy to remove the policy.</div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Close</button> <button type=\"button\" class=\"btn btn-outline-danger\" onclick=\"deleteS3TablesBucketPolicy()\"><i class=\"fas fa-trash me-1\"></i>Delete Policy</button> <button type=\"submit\" class=\"btn btn-primary\"><i class=\"fas fa-save me-1\"></i>Save Policy</button></div></form></div></div></div><div class=\"modal fade\" id=\"s3tablesTagsModal\" tabindex=\"-1\" aria-labelledby=\"s3tablesTagsModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog modal-lg\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"s3tablesTagsModalLabel\"><i class=\"fas fa-tags me-2\"></i>Resource Tags</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><form id=\"s3tablesTagsForm\"><div class=\"modal-body\"><input type=\"hidden\" id=\"s3tablesTagsResourceArn\" name=\"resource_arn\"><div class=\"mb-3\"><label class=\"form-label\">Existing Tags</label><pre class=\"bg-light p-3 border rounded\" id=\"s3tablesTagsList\">Loading...</pre></div><div class=\"mb-3\"><label for=\"s3tablesTagsInput\" class=\"form-label\">Add or Update Tags</label> <input type=\"text\" class=\"form-control\" id=\"s3tablesTagsInput\" placeholder=\"key1=value1,key2=value2\"></div><div class=\"mb-3\"><label for=\"s3tablesTagsDeleteInput\" class=\"form-label\">Remove Tag Keys</label> <input type=\"text\" class=\"form-control\" id=\"s3tablesTagsDeleteInput\" placeholder=\"key1,key2\"></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Close</button> <button type=\"button\" class=\"btn btn-outline-danger\" onclick=\"deleteS3TablesTags()\"><i class=\"fas fa-trash me-1\"></i>Remove Tags</button> <button type=\"submit\" class=\"btn btn-primary\"><i class=\"fas fa-save me-1\"></i>Update Tags</button></div></form></div></div></div><script>\n\t\tlet s3tablesBucketDeleteModal = null;\n\t\tlet s3tablesBucketPolicyModal = null;\n\t\tlet s3tablesTagsModal = null;\n\n\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\ts3tablesBucketDeleteModal = new bootstrap.Modal(document.getElementById('deleteS3TablesBucketModal'));\n\t\t\ts3tablesBucketPolicyModal = new bootstrap.Modal(document.getElementById('s3tablesBucketPolicyModal'));\n\t\t\ts3tablesTagsModal = new bootstrap.Modal(document.getElementById('s3tablesTagsModal'));\n\t\t\tconst ownerSelect = document.getElementById('s3tablesBucketOwner');\n\n\t\t\tdocument.getElementById('createS3TablesBucketModal').addEventListener('show.bs.modal', async function() {\n\t\t\t\tif (ownerSelect.options.length <= 1) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst response = await fetch('/api/users');\n\t\t\t\t\t\tconst data = await response.json();\n\t\t\t\t\t\tconst users = data.users || [];\n\t\t\t\t\t\tusers.forEach(user => {\n\t\t\t\t\t\t\tconst option = document.createElement('option');\n\t\t\t\t\t\t\toption.value = user.username;\n\t\t\t\t\t\t\toption.textContent = user.username;\n\t\t\t\t\t\t\townerSelect.appendChild(option);\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.error('Error fetching users for owner dropdown:', error);\n\t\t\t\t\t\townerSelect.innerHTML = '<option value=\"\">No owner (admin-only access)</option>';\n\t\t\t\t\t\townerSelect.selectedIndex = 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tdocument.querySelectorAll('.s3tables-delete-bucket-btn').forEach(button => {\n\t\t\t\tbutton.addEventListener('click', function() {\n\t\t\t\t\tdocument.getElementById('deleteS3TablesBucketName').textContent = this.dataset.bucketName || '';\n\t\t\t\t\tdocument.getElementById('deleteS3TablesBucketModal').dataset.bucketArn = this.dataset.bucketArn || '';\n\t\t\t\t\ts3tablesBucketDeleteModal.show();\n\t\t\t\t});\n\t\t\t});\n\n\t\t\tdocument.querySelectorAll('.s3tables-bucket-policy-btn').forEach(button => {\n\t\t\t\tbutton.addEventListener('click', function() {\n\t\t\t\t\tconst bucketArn = this.dataset.bucketArn || '';\n\t\t\t\t\tdocument.getElementById('s3tablesBucketPolicyArn').value = bucketArn;\n\t\t\t\t\tloadS3TablesBucketPolicy(bucketArn);\n\t\t\t\t\ts3tablesBucketPolicyModal.show();\n\t\t\t\t});\n\t\t\t});\n\n\t\t\tdocument.querySelectorAll('.s3tables-tags-btn').forEach(button => {\n\t\t\t\tbutton.addEventListener('click', function() {\n\t\t\t\t\tconst resourceArn = this.dataset.resourceArn || '';\n\t\t\t\t\topenS3TablesTags(resourceArn);\n\t\t\t\t});\n\t\t\t});\n\n\t\t\tdocument.getElementById('createS3TablesBucketForm').addEventListener('submit', async function(e) {\n\t\t\t\te.preventDefault();\n\t\t\t\tconst name = document.getElementById('s3tablesBucketName').value.trim();\n\t\t\t\tconst owner = ownerSelect.value;\n\t\t\t\tconst tagsInput = document.getElementById('s3tablesBucketTags').value.trim();\n\t\t\t\tconst tags = parseTagsInput(tagsInput);\n\t\t\t\tif (tags === null) return;\n\t\t\t\tconst payload = { name: name, tags: tags, owner: owner };\n\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await fetch('/api/s3tables/buckets', {\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\t\t\t\tbody: JSON.stringify(payload)\n\t\t\t\t\t});\n\t\t\t\t\tconst data = await response.json();\n\t\t\t\t\tif (!response.ok) {\n\t\t\t\t\t\talert(data.error || 'Failed to create bucket');\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\talert('Bucket created successfully');\n\t\t\t\t\tlocation.reload();\n\t\t\t\t} catch (error) {\n\t\t\t\t\talert('Failed to create bucket: ' + error.message);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tdocument.getElementById('s3tablesBucketPolicyForm').addEventListener('submit', async function(e) {\n\t\t\t\te.preventDefault();\n\t\t\t\tconst bucketArn = document.getElementById('s3tablesBucketPolicyArn').value;\n\t\t\t\tconst policy = document.getElementById('s3tablesBucketPolicyText').value.trim();\n\t\t\t\tif (!policy) {\n\t\t\t\t\talert('Policy JSON is required');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await fetch('/api/s3tables/bucket-policy', {\n\t\t\t\t\t\tmethod: 'PUT',\n\t\t\t\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\t\t\t\tbody: JSON.stringify({ bucket_arn: bucketArn, policy: policy })\n\t\t\t\t\t});\n\t\t\t\t\tconst data = await response.json();\n\t\t\t\t\tif (!response.ok) {\n\t\t\t\t\t\talert(data.error || 'Failed to update policy');\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\talert('Policy updated');\n\t\t\t\t\ts3tablesBucketPolicyModal.hide();\n\t\t\t\t} catch (error) {\n\t\t\t\t\talert('Failed to update policy: ' + error.message);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tdocument.getElementById('s3tablesTagsForm').addEventListener('submit', async function(e) {\n\t\t\t\te.preventDefault();\n\t\t\t\tconst resourceArn = document.getElementById('s3tablesTagsResourceArn').value;\n\t\t\t\tconst tags = parseTagsInput(document.getElementById('s3tablesTagsInput').value.trim());\n\t\t\t\tif (tags === null || Object.keys(tags).length === 0) {\n\t\t\t\t\talert('Please provide tags to update');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait updateS3TablesTags(resourceArn, tags);\n\t\t\t});\n\t\t});\n\n\t\tasync function deleteS3TablesBucket() {\n\t\t\tconst bucketArn = document.getElementById('deleteS3TablesBucketModal').dataset.bucketArn;\n\t\t\tif (!bucketArn) return;\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(`/api/s3tables/buckets?bucket=${encodeURIComponent(bucketArn)}`, { method: 'DELETE' });\n\t\t\t\tconst data = await response.json();\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\talert(data.error || 'Failed to delete bucket');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\talert('Bucket deleted');\n\t\t\t\tlocation.reload();\n\t\t\t} catch (error) {\n\t\t\t\talert('Failed to delete bucket: ' + error.message);\n\t\t\t}\n\t\t}\n\n\t\tasync function loadS3TablesBucketPolicy(bucketArn) {\n\t\t\tdocument.getElementById('s3tablesBucketPolicyText').value = '';\n\t\t\tif (!bucketArn) return;\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(`/api/s3tables/bucket-policy?bucket=${encodeURIComponent(bucketArn)}`);\n\t\t\t\tconst data = await response.json();\n\t\t\t\tif (response.ok && data.policy) {\n\t\t\t\t\tdocument.getElementById('s3tablesBucketPolicyText').value = data.policy;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('Failed to load bucket policy', error);\n\t\t\t}\n\t\t}\n\n\t\tasync function deleteS3TablesBucketPolicy() {\n\t\t\tconst bucketArn = document.getElementById('s3tablesBucketPolicyArn').value;\n\t\t\tif (!bucketArn) return;\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(`/api/s3tables/bucket-policy?bucket=${encodeURIComponent(bucketArn)}`, { method: 'DELETE' });\n\t\t\t\tconst data = await response.json();\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\talert(data.error || 'Failed to delete policy');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\talert('Policy deleted');\n\t\t\t\tdocument.getElementById('s3tablesBucketPolicyText').value = '';\n\t\t\t} catch (error) {\n\t\t\t\talert('Failed to delete policy: ' + error.message);\n\t\t\t}\n\t\t}\n\n\t\tfunction parseTagsInput(input) {\n\t\t\tif (!input) return {};\n\t\t\tconst tags = {};\n\t\t\tconst maxTags = 10;\n\t\t\tconst maxKeyLength = 128;\n\t\t\tconst maxValueLength = 256;\n\t\t\tconst parts = input.split(',');\n\t\t\tfor (const part of parts) {\n\t\t\t\tconst trimmedPart = part.trim();\n\t\t\t\tif (!trimmedPart) continue;\n\t\t\t\tconst idx = trimmedPart.indexOf('=');\n\t\t\t\tif (idx <= 0) {\n\t\t\t\t\talert('Invalid tag format. Use key=value, and key cannot be empty.');\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tconst key = trimmedPart.slice(0, idx).trim();\n\t\t\t\tconst value = trimmedPart.slice(idx + 1).trim();\n\t\t\t\tif (!key) {\n\t\t\t\t\talert('Invalid tag format. Use key=value, and key cannot be empty.');\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tif (key.length > maxKeyLength) {\n\t\t\t\t\talert(`Tag key length must be <= ${maxKeyLength}`);\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tif (value.length > maxValueLength) {\n\t\t\t\t\talert(`Tag value length must be <= ${maxValueLength}`);\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\ttags[key] = value;\n\t\t\t\tif (Object.keys(tags).length > maxTags) {\n\t\t\t\t\talert(`Too many tags. Max ${maxTags} tags allowed.`);\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn tags;\n\t\t}\n\n\t\tasync function openS3TablesTags(resourceArn) {\n\t\t\tif (!resourceArn) return;\n\t\t\tdocument.getElementById('s3tablesTagsResourceArn').value = resourceArn;\n\t\t\tdocument.getElementById('s3tablesTagsInput').value = '';\n\t\t\tdocument.getElementById('s3tablesTagsDeleteInput').value = '';\n\t\t\tdocument.getElementById('s3tablesTagsList').textContent = 'Loading...';\n\t\t\ts3tablesTagsModal.show();\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(`/api/s3tables/tags?arn=${encodeURIComponent(resourceArn)}`);\n\t\t\t\tconst data = await response.json();\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tdocument.getElementById('s3tablesTagsList').textContent = JSON.stringify(data.tags || {}, null, 2);\n\t\t\t\t} else {\n\t\t\t\t\tdocument.getElementById('s3tablesTagsList').textContent = data.error || 'Failed to load tags';\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tdocument.getElementById('s3tablesTagsList').textContent = error.message;\n\t\t\t}\n\t\t}\n\n\t\tasync function updateS3TablesTags(resourceArn, tags) {\n\t\t\ttry {\n\t\t\t\tconst response = await fetch('/api/s3tables/tags', {\n\t\t\t\t\tmethod: 'PUT',\n\t\t\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\t\t\tbody: JSON.stringify({ resource_arn: resourceArn, tags: tags })\n\t\t\t\t});\n\t\t\t\tconst data = await response.json();\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\talert(data.error || 'Failed to update tags');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\talert('Tags updated');\n\t\t\t\topenS3TablesTags(resourceArn);\n\t\t\t} catch (error) {\n\t\t\t\talert('Failed to update tags: ' + error.message);\n\t\t\t}\n\t\t}\n\n\t\tasync function deleteS3TablesTags() {\n\t\t\tconst resourceArn = document.getElementById('s3tablesTagsResourceArn').value;\n\t\t\tconst keysInput = document.getElementById('s3tablesTagsDeleteInput').value.trim();\n\t\t\tif (!resourceArn) return;\n\t\t\tconst tagKeys = keysInput.split(',').map(k => k.trim()).filter(k => k);\n\t\t\tif (tagKeys.length === 0) {\n\t\t\t\talert('Provide tag keys to remove');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst response = await fetch('/api/s3tables/tags', {\n\t\t\t\t\tmethod: 'DELETE',\n\t\t\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\t\t\tbody: JSON.stringify({ resource_arn: resourceArn, tag_keys: tagKeys })\n\t\t\t\t});\n\t\t\t\tconst data = await response.json();\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\talert(data.error || 'Failed to remove tags');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\talert('Tags removed');\n\t\t\t\topenS3TablesTags(resourceArn);\n\t\t\t} catch (error) {\n\t\t\t\talert('Failed to remove tags: ' + error.message);\n\t\t\t}\n\t\t}\n\t</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate