* Add Iceberg table details view
* Enhance Iceberg catalog browsing UI
* Fix Iceberg UI security and logic issues
- Fix selectSchema() and partitionFieldsFromFullMetadata() to always search for matching IDs instead of checking != 0
- Fix snapshotsFromFullMetadata() to defensive-copy before sorting to prevent mutating caller's slice
- Fix XSS vulnerabilities in s3tables.js: replace innerHTML with textContent/createElement for user-controlled data
- Fix deleteIcebergTable() to redirect to namespace tables list on details page instead of reloading
- Fix data-bs-target in iceberg_namespaces.templ: remove templ.SafeURL for CSS selector
- Add catalogName to delete modal data attributes for proper redirect
- Remove unused hidden inputs from create table form (icebergTableBucketArn, icebergTableNamespace)
* Regenerate templ files for Iceberg UI updates
* Support complex Iceberg type objects in schema
Change Type field from string to json.RawMessage in both IcebergSchemaFieldInfo
and internal icebergSchemaField to properly handle Iceberg spec's complex type
objects (e.g. {"type": "struct", "fields": [...]}). Currently test data
only shows primitive string types, but this change makes the implementation
defensively robust for future complex types by preserving the exact JSON
representation. Add typeToString() helper and update schema extraction
functions to marshal string types as JSON. Update template to convert
json.RawMessage to string for display.
* Regenerate templ files for Type field changes
* templ
* Fix additional Iceberg UI issues from code review
- Fix lazy-load flag that was set before async operation completed, preventing retries
on error; now sets loaded flag only after successful load and throws error to caller
for proper error handling and UI updates
- Add zero-time guards for CreatedAt and ModifiedAt fields in table details to avoid
displaying Go zero-time values; render dash when time is zero
- Add URL path escaping for all catalog/namespace/table names in URLs to prevent
malformed URLs when names contain special characters like /, ?, or #
- Remove redundant innerHTML clear in loadIcebergNamespaceTables that cleared twice
before appending the table list
- Fix selectSnapshotForMetrics to remove != 0 guard for consistency with selectSchema
fix; now always searches for CurrentSnapshotID without zero-value gate
- Enhance typeToString() helper to display '(complex)' for non-primitive JSON types
* Regenerate templ files for Phase 3 updates
* Fix template generation to use correct file paths
Run templ generate from repo root instead of weed/admin directory to ensure
generated _templ.go files have correct absolute paths in error messages
(e.g., 'weed/admin/view/app/iceberg_table_details.templ' instead of
'app/iceberg_table_details.templ'). This ensures both 'make admin-generate'
at repo root and 'make generate' in weed/admin directory produce identical
output with consistent file path references.
* Regenerate template files with correct path references
* Validate S3 Tables names in UI
- Add client-side validation for table bucket and namespace names to surface
errors for invalid characters (dots/underscores) before submission
- Use HTML validity messages with reportValidity for immediate feedback
- Update namespace helper text to reflect actual constraints (single-level,
lowercase letters, numbers, and underscores)
* Regenerate templ files for namespace helper text
* Fix Iceberg catalog REST link and actions
* Disallow S3 object access on table buckets
* Validate Iceberg layout for table bucket objects
* Fix REST API link to /v1/config
* merge iceberg page with table bucket page
* Allowed Trino/Iceberg stats files in metadata validation
* fixes
- Backend/data handling:
- Normalized Iceberg type display and fallback handling in weed/admin/dash/s3tables_management.go.
- Fixed snapshot fallback pointer semantics in weed/admin/dash/s3tables_management.go.
- Added CSRF token generation/propagation/validation for namespace create/delete in:
- weed/admin/dash/csrf.go
- weed/admin/dash/auth_middleware.go
- weed/admin/dash/middleware.go
- weed/admin/dash/s3tables_management.go
- weed/admin/view/layout/layout.templ
- weed/admin/static/js/s3tables.js
- UI/template fixes:
- Zero-time guards for CreatedAt fields in:
- weed/admin/view/app/iceberg_namespaces.templ
- weed/admin/view/app/iceberg_tables.templ
- Fixed invalid templ-in-script interpolation and host/port rendering in:
- weed/admin/view/app/iceberg_catalog.templ
- weed/admin/view/app/s3tables_buckets.templ
- Added data-catalog-name consistency on Iceberg delete action in weed/admin/view/app/iceberg_tables.templ.
- Updated retry wording in weed/admin/static/js/s3tables.js.
- Regenerated all affected _templ.go files.
- S3 API/comment follow-ups:
- Reused cached table-bucket validator in weed/s3api/bucket_paths.go.
- Added validation-failure debug logging in weed/s3api/s3api_object_handlers_tagging.go.
- Added multipart path-validation design comment in weed/s3api/s3api_object_handlers_multipart.go.
- Build tooling:
- Fixed templ generate working directory issues in weed/admin/Makefile (watch + pattern rule).
* populate data
* test/s3tables: harden populate service checks
* admin: skip table buckets in object-store bucket list
* admin sidebar: move object store to top-level links
* admin iceberg catalog: guard zero times and escape links
* admin forms: add csrf/error handling and client-side name validation
* admin s3tables: fix namespace delete modal redeclaration
* admin: replace native confirm dialogs with modal helpers
* admin modal-alerts: remove noisy confirm usage console log
* reduce logs
* test/s3tables: use partitioned tables in trino and spark populate
* admin file browser: normalize filer ServerAddress for HTTP parsing
284 lines
36 KiB
Go
284 lines
36 KiB
Go
// Code generated by templ - DO NOT EDIT.
|
|
|
|
// templ: version: v0.3.977
|
|
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 ServiceAccounts(data dash.ServiceAccountsData) 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=\"container-fluid\"><!-- Page Header --><div class=\"d-sm-flex align-items-center justify-content-between mb-4\"><div><h1 class=\"h3 mb-0 text-gray-800\"><i class=\"fas fa-robot me-2\"></i>Service Accounts</h1><p class=\"mb-0 text-muted\">Manage application credentials for automated processes</p></div><div class=\"d-flex gap-2\"><button type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#createServiceAccountModal\"><i class=\"fas fa-plus me-1\"></i>Create Service Account</button></div></div><!-- Summary Cards --><div class=\"row mb-4\"><div class=\"col-xl-3 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 Service Accounts</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.TotalAccounts))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/service_accounts.templ`, Line: 38, Col: 74}
|
|
}
|
|
_, 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-id-card fa-2x text-gray-300\"></i></div></div></div></div></div><div class=\"col-xl-3 col-md-6 mb-4\"><div class=\"card border-left-success shadow h-100 py-2\"><div class=\"card-body\"><div class=\"row no-gutters align-items-center\"><div class=\"col mr-2\"><div class=\"text-xs font-weight-bold text-success text-uppercase mb-1\">Active Accounts</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(fmt.Sprintf("%d", data.ActiveAccounts))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/service_accounts.templ`, Line: 58, Col: 75}
|
|
}
|
|
_, 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-check-circle fa-2x text-gray-300\"></i></div></div></div></div></div><div class=\"col-xl-3 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_Var4 string
|
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("15:04"))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/service_accounts.templ`, Line: 78, Col: 69}
|
|
}
|
|
_, 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, 4, "</div></div><div class=\"col-auto\"><i class=\"fas fa-clock fa-2x text-gray-300\"></i></div></div></div></div></div></div><!-- Service Accounts Table --><div class=\"row\"><div class=\"col-12\"><div class=\"card shadow mb-4\"><div class=\"card-header py-3 d-flex flex-row align-items-center justify-content-between\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-robot me-2\"></i>Service Accounts</h6></div><div class=\"card-body\"><div class=\"table-responsive\"><table class=\"table table-hover\" width=\"100%\" cellspacing=\"0\" id=\"serviceAccountsTable\"><thead><tr><th>ID</th><th>Parent User</th><th>Access Key</th><th>Status</th><th>Created</th><th>Actions</th></tr></thead> <tbody>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
for _, sa := range data.ServiceAccounts {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<tr><td><div class=\"d-flex align-items-center\"><i class=\"fas fa-robot me-2 text-muted\"></i> <code>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var5 string
|
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(sa.ID)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/service_accounts.templ`, Line: 118, Col: 64}
|
|
}
|
|
_, 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, "</code></div></td><td><i class=\"fas fa-user me-1 text-muted\"></i> ")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var6 string
|
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(sa.ParentUser)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/service_accounts.templ`, Line: 123, Col: 62}
|
|
}
|
|
_, 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><code class=\"text-muted\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var7 string
|
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(sa.AccessKeyId)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/service_accounts.templ`, Line: 126, Col: 88}
|
|
}
|
|
_, 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, "</code></td><td>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if sa.Status == "Active" {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span class=\"badge bg-success\">Active</span>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span class=\"badge bg-secondary\">Inactive</span>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</td><td>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var8 string
|
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(sa.CreateDate.Format("2006-01-02"))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/service_accounts.templ`, Line: 135, Col: 83}
|
|
}
|
|
_, 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, 12, "</td><td><div class=\"btn-group btn-group-sm\" role=\"group\"><button type=\"button\" class=\"btn btn-outline-info\" data-action=\"show-sa-details\" data-sa-id=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var9 string
|
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(sa.ID)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/service_accounts.templ`, Line: 139, Col: 108}
|
|
}
|
|
_, 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, "\"><i class=\"fas fa-info-circle\"></i></button> <button type=\"button\" class=\"btn btn-outline-primary\" data-action=\"toggle-sa-status\" data-sa-id=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var10 string
|
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(sa.ID)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/service_accounts.templ`, Line: 143, Col: 109}
|
|
}
|
|
_, 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, "\" data-current-status=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var11 string
|
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(sa.Status)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/service_accounts.templ`, Line: 143, Col: 143}
|
|
}
|
|
_, 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, "\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
if sa.Status == "Active" {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<i class=\"fas fa-pause\"></i>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
} else {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<i class=\"fas fa-play\"></i>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</button> <button type=\"button\" class=\"btn btn-outline-danger\" data-action=\"delete-sa\" data-sa-id=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var12 string
|
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(sa.ID)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/service_accounts.templ`, Line: 151, Col: 102}
|
|
}
|
|
_, 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, 19, "\"><i class=\"fas fa-trash\"></i></button></div></td></tr>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
if len(data.ServiceAccounts) == 0 {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<tr><td colspan=\"6\" class=\"text-center text-muted py-4\"><i class=\"fas fa-robot fa-3x mb-3 text-muted\"></i><div><h5>No service accounts found</h5><p>Create your first service account for automated processes.</p></div></td></tr>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</tbody></table></div></div></div></div></div><!-- Last Updated --><div class=\"row\"><div class=\"col-12\"><small class=\"text-muted\"><i class=\"fas fa-clock me-1\"></i> Last updated: ")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var13 string
|
|
templ_7745c5c3_Var13, 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: `weed/admin/view/app/service_accounts.templ`, Line: 182, Col: 81}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</small></div></div></div><!-- Create Service Account Modal --><div class=\"modal fade\" id=\"createServiceAccountModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-plus me-2\"></i>Create Service Account</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\"><form id=\"createSAForm\"><div class=\"mb-3\"><label for=\"parentUser\" class=\"form-label\">Parent User *</label> <select class=\"form-select\" id=\"parentUser\" name=\"parent_user\" required><option value=\"\">-- Select a user --</option> ")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
for _, user := range data.AvailableUsers {
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<option value=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var14 string
|
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(user)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/service_accounts.templ`, Line: 205, Col: 56}
|
|
}
|
|
_, 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, 24, "\">")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var15 string
|
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(user)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/service_accounts.templ`, Line: 205, Col: 65}
|
|
}
|
|
_, 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, 25, "</option>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</select> <small class=\"form-text text-muted\">The service account will inherit permissions from this user</small></div><div class=\"mb-3\"><label for=\"description\" class=\"form-label\">Description</label> <textarea class=\"form-control\" id=\"description\" name=\"description\" rows=\"2\" placeholder=\"What is this service account used for?\"></textarea></div><div class=\"mb-3\"><label for=\"expiration\" class=\"form-label\">Expiration (optional)</label> <input type=\"datetime-local\" class=\"form-control\" id=\"expiration\" name=\"expiration\"> <small class=\"form-text text-muted\">Leave empty for no expiration</small></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"handleCreateServiceAccount()\">Create</button></div></div></div></div><!-- Service Account Details Modal --><div class=\"modal fade\" id=\"saDetailsModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog modal-lg\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-robot me-2\"></i>Service Account Details</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\" id=\"saDetailsContent\"><!-- Content will be loaded dynamically --></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Close</button></div></div></div></div><!-- Credentials Display Modal --><div class=\"modal fade\" id=\"credentialsModal\" tabindex=\"-1\" role=\"dialog\" data-bs-backdrop=\"static\" data-bs-keyboard=\"false\"><div class=\"modal-dialog modal-lg\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header bg-success text-white\"><h5 class=\"modal-title\"><i class=\"fas fa-check-circle me-2\"></i>Service Account Created Successfully</h5></div><div class=\"modal-body\"><div class=\"alert alert-warning\"><i class=\"fas fa-exclamation-triangle me-2\"></i> <strong>Important:</strong> This is the only time you will see the secret access key. Please save it securely.</div><div class=\"mb-4\"><h6 class=\"text-muted mb-3\">AWS CLI Configuration</h6><p class=\"text-muted small\">Use these credentials to configure AWS CLI or SDKs:</p><div class=\"mb-3\"><label class=\"form-label fw-bold\">AWS_ACCESS_KEY_ID</label><div class=\"input-group\"><input type=\"text\" class=\"form-control font-monospace\" id=\"displayAccessKey\" readonly> <button class=\"btn btn-outline-secondary\" type=\"button\" onclick=\"copyCredentialToClipboard(this, 'displayAccessKey')\"><i class=\"fas fa-copy\"></i> Copy</button></div></div><div class=\"mb-3\"><label class=\"form-label fw-bold\">AWS_SECRET_ACCESS_KEY</label><div class=\"input-group\"><input type=\"text\" class=\"form-control font-monospace\" id=\"displaySecretKey\" readonly> <button class=\"btn btn-outline-secondary\" type=\"button\" onclick=\"copyCredentialToClipboard(this, 'displaySecretKey')\"><i class=\"fas fa-copy\"></i> Copy</button></div></div></div><div class=\"bg-light p-3 rounded\"><h6 class=\"text-muted mb-2\">Example AWS CLI Usage:</h6><div class=\"font-monospace small\"><div>export AWS_ACCESS_KEY_ID=<span id=\"exampleAccessKey\"></span></div><div>export AWS_SECRET_ACCESS_KEY=<span id=\"exampleSecretKey\"></span></div><div>export AWS_ENDPOINT_URL=http://localhost:8333</div><div class=\"mt-2\"># List buckets</div><div>aws s3 ls</div><div class=\"mt-2\"># Upload a file</div><div>aws s3 cp myfile.txt s3://mybucket/</div></div></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-primary\" onclick=\"closeCredentialsModal()\"><i class=\"fas fa-check me-1\"></i>I have saved the credentials</button></div></div></div></div><!-- JavaScript for service account management --><script>\n document.addEventListener('DOMContentLoaded', function() {\n document.addEventListener('click', function(e) {\n const button = e.target.closest('[data-action]');\n if (!button) return;\n \n const action = button.getAttribute('data-action');\n const saId = button.getAttribute('data-sa-id');\n \n switch (action) {\n case 'show-sa-details':\n showSADetails(saId);\n break;\n case 'toggle-sa-status':\n toggleSAStatus(saId, button.getAttribute('data-current-status'));\n break;\n case 'delete-sa':\n deleteSA(saId);\n break;\n }\n });\n });\n\n async function showSADetails(id) {\n try {\n const encodedId = encodeURIComponent(id);\n const response = await fetch(`/api/service-accounts/${encodedId}`);\n if (response.ok) {\n const sa = await response.json();\n document.getElementById('saDetailsContent').innerHTML = createSADetailsContent(sa);\n const modal = new bootstrap.Modal(document.getElementById('saDetailsModal'));\n modal.show();\n } else {\n showErrorMessage('Failed to load service account details');\n }\n } catch (error) {\n console.error('Error loading service account details:', error);\n showErrorMessage('Failed to load service account details');\n }\n }\n\n async function toggleSAStatus(id, currentStatus) {\n const newStatus = currentStatus === 'Active' ? 'Inactive' : 'Active';\n try {\n const encodedId = encodeURIComponent(id);\n const response = await fetch(`/api/service-accounts/${encodedId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ status: newStatus })\n });\n \n if (response.ok) {\n showSuccessMessage(`Service account ${newStatus === 'Active' ? 'activated' : 'deactivated'}`);\n setTimeout(() => window.location.reload(), 1000);\n } else {\n const error = await response.json();\n showErrorMessage('Failed to update status: ' + (error.error || 'Unknown error'));\n }\n } catch (error) {\n console.error('Error updating status:', error);\n showErrorMessage('Failed to update status: ' + error.message);\n }\n }\n\n async function deleteSA(id) {\n showDeleteConfirm(id, async function() {\n try {\n const encodedId = encodeURIComponent(id);\n const response = await fetch(`/api/service-accounts/${encodedId}`, {\n method: 'DELETE'\n });\n \n if (response.ok) {\n showSuccessMessage('Service account deleted successfully');\n setTimeout(() => window.location.reload(), 1000);\n } else {\n const error = await response.json();\n showErrorMessage('Failed to delete: ' + (error.error || 'Unknown error'));\n }\n } catch (error) {\n console.error('Error deleting service account:', error);\n showErrorMessage('Failed to delete: ' + error.message);\n }\n }, 'Are you sure you want to delete this service account? This action cannot be undone.');\n }\n\n async function handleCreateServiceAccount() {\n const form = document.getElementById('createSAForm');\n const formData = new FormData(form);\n \n const saData = {\n parent_user: formData.get('parent_user'),\n description: formData.get('description')\n };\n \n // Handle expiration if set\n const expiration = formData.get('expiration');\n if (expiration) {\n // Validate the date before using it\n const date = new Date(expiration);\n const now = new Date();\n \n if (isNaN(date.getTime())) {\n showErrorMessage('Invalid expiration date format');\n return;\n }\n \n // Ensure expiration is in the future\n if (date <= now) {\n showErrorMessage('Expiration date must be in the future');\n return;\n }\n \n saData.expiration = date.toISOString();\n }\n \n try {\n const response = await fetch('/api/service-accounts', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(saData)\n });\n \n if (response.ok) {\n const result = await response.json();\n \n // Hide create modal\n const createModal = bootstrap.Modal.getInstance(document.getElementById('createServiceAccountModal'));\n createModal.hide();\n form.reset();\n \n // Show credentials if returned\n if (result.service_account && result.service_account.secret_access_key) {\n showCredentials(result.service_account);\n } else {\n showSuccessMessage('Service account created successfully');\n setTimeout(() => window.location.reload(), 1000);\n }\n } else {\n const error = await response.json();\n showErrorMessage('Failed to create service account: ' + (error.error || 'Unknown error'));\n }\n } catch (error) {\n console.error('Error creating service account:', error);\n showErrorMessage('Failed to create service account: ' + error.message);\n }\n }\n\n function showCredentials(serviceAccount) {\n // Populate the credentials modal\n document.getElementById('displayAccessKey').value = serviceAccount.access_key_id;\n document.getElementById('displaySecretKey').value = serviceAccount.secret_access_key;\n document.getElementById('exampleAccessKey').textContent = serviceAccount.access_key_id;\n document.getElementById('exampleSecretKey').textContent = serviceAccount.secret_access_key;\n \n // Show the modal\n const credentialsModal = new bootstrap.Modal(document.getElementById('credentialsModal'));\n credentialsModal.show();\n }\n\n function closeCredentialsModal() {\n const modal = bootstrap.Modal.getInstance(document.getElementById('credentialsModal'));\n modal.hide();\n // Reload to show the new service account in the list\n setTimeout(() => window.location.reload(), 500);\n }\n\n function copyCredentialToClipboard(button, elementId) {\n const element = document.getElementById(elementId);\n const textToCopy = element.value;\n \n // Use modern Clipboard API if available\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(textToCopy).then(() => {\n showSuccessMessage('Copied to clipboard!');\n }).catch(err => {\n console.warn('Clipboard API failed:', err);\n // Fallback\n fallbackCopyTextToClipboard(element);\n });\n } else {\n // Fallback for older browsers or non-secure contexts\n fallbackCopyTextToClipboard(element);\n }\n \n // Visual feedback\n const originalHTML = button.innerHTML;\n \n button.innerHTML = '<i class=\"fas fa-check\"></i>';\n button.classList.remove('btn-outline-secondary');\n button.classList.add('btn-success');\n \n setTimeout(() => {\n button.innerHTML = originalHTML;\n button.classList.remove('btn-success');\n button.classList.add('btn-outline-secondary');\n }, 1000);\n }\n\n function fallbackCopyTextToClipboard(element) {\n element.select();\n element.setSelectionRange(0, 99999); // For mobile devices\n \n try {\n const successful = document.execCommand('copy');\n if (successful) {\n showSuccessMessage('Copied to clipboard!');\n } else {\n showErrorMessage('Failed to copy to clipboard');\n }\n } catch (err) {\n console.error('Fallback copy failed:', err);\n showErrorMessage('Failed to copy to clipboard');\n }\n \n // Clear selection\n window.getSelection().removeAllRanges();\n }\n\n\n function createSADetailsContent(sa) {\n // Create DOM elements safely to prevent XSS\n const container = document.createElement('div');\n container.className = 'row';\n \n // Basic Information column\n const col1 = document.createElement('div');\n col1.className = 'col-md-6';\n \n const h6_1 = document.createElement('h6');\n h6_1.className = 'text-muted';\n h6_1.textContent = 'Basic Information';\n col1.appendChild(h6_1);\n \n const table1 = document.createElement('table');\n table1.className = 'table table-sm';\n \n // ID row\n const idRow = document.createElement('tr');\n idRow.innerHTML = '<td><strong>ID:</strong></td><td><code></code></td>';\n idRow.querySelector('code').textContent = sa.id || '';\n table1.appendChild(idRow);\n \n // Parent User row\n const parentRow = document.createElement('tr');\n parentRow.innerHTML = '<td><strong>Parent User:</strong></td><td></td>';\n parentRow.querySelectorAll('td')[1].textContent = sa.parent_user || '';\n table1.appendChild(parentRow);\n \n // Access Key row\n const keyRow = document.createElement('tr');\n keyRow.innerHTML = '<td><strong>Access Key:</strong></td><td><code></code></td>';\n keyRow.querySelector('code').textContent = sa.access_key_id || '';\n table1.appendChild(keyRow);\n \n // Status row\n const statusRow = document.createElement('tr');\n const statusTd1 = document.createElement('td');\n statusTd1.innerHTML = '<strong>Status:</strong>';\n const statusTd2 = document.createElement('td');\n const statusBadge = document.createElement('span');\n statusBadge.className = sa.status === 'Active' ? 'badge bg-success' : 'badge bg-secondary';\n statusBadge.textContent = sa.status || 'Unknown';\n statusTd2.appendChild(statusBadge);\n statusRow.appendChild(statusTd1);\n statusRow.appendChild(statusTd2);\n table1.appendChild(statusRow);\n \n col1.appendChild(table1);\n container.appendChild(col1);\n \n // Details column\n const col2 = document.createElement('div');\n col2.className = 'col-md-6';\n \n const h6_2 = document.createElement('h6');\n h6_2.className = 'text-muted';\n h6_2.textContent = 'Details';\n col2.appendChild(h6_2);\n \n const table2 = document.createElement('table');\n table2.className = 'table table-sm';\n \n // Description row\n const descRow = document.createElement('tr');\n descRow.innerHTML = '<td><strong>Description:</strong></td><td></td>';\n descRow.querySelectorAll('td')[1].textContent = sa.description || 'Not set';\n table2.appendChild(descRow);\n \n // Created row\n const createdRow = document.createElement('tr');\n createdRow.innerHTML = '<td><strong>Created:</strong></td><td></td>';\n try {\n createdRow.querySelectorAll('td')[1].textContent = new Date(sa.create_date).toLocaleString();\n } catch (e) {\n createdRow.querySelectorAll('td')[1].textContent = 'Invalid date';\n }\n table2.appendChild(createdRow);\n \n // Expiration row\n const expRow = document.createElement('tr');\n expRow.innerHTML = '<td><strong>Expires:</strong></td><td></td>';\n expRow.querySelectorAll('td')[1].textContent = sa.expiration || 'Never';\n table2.appendChild(expRow);\n \n col2.appendChild(table2);\n container.appendChild(col2);\n \n return container.outerHTML;\n }\n\n\n function showSuccessMessage(message) {\n showToast(message, 'success');\n }\n\n function showErrorMessage(message) {\n showToast(message, 'danger');\n }\n\n function showToast(message, type) {\n // Create toast container if it doesn't exist\n let toastContainer = document.getElementById('toastContainer');\n if (!toastContainer) {\n toastContainer = document.createElement('div');\n toastContainer.id = 'toastContainer';\n toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';\n toastContainer.style.zIndex = '9999';\n document.body.appendChild(toastContainer);\n }\n\n // Create toast element\n const toastId = 'toast-' + Date.now();\n const toastHTML = `\n <div id=\"${toastId}\" class=\"toast align-items-center text-white bg-${type} border-0\" role=\"alert\" aria-live=\"assertive\" aria-atomic=\"true\">\n <div class=\"d-flex\">\n <div class=\"toast-body\">\n ${escapeHtml(message)}\n </div>\n <button type=\"button\" class=\"btn-close btn-close-white me-2 m-auto\" data-bs-dismiss=\"toast\" aria-label=\"Close\"></button>\n </div>\n </div>\n `;\n \n toastContainer.insertAdjacentHTML('beforeend', toastHTML);\n const toastElement = document.getElementById(toastId);\n const toast = new bootstrap.Toast(toastElement, { autohide: true, delay: 5000 });\n toast.show();\n \n // Remove toast element after it's hidden\n toastElement.addEventListener('hidden.bs.toast', () => {\n toastElement.remove();\n });\n }\n\n function escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n </script>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
var _ = templruntime.GeneratedTemplate
|