From 79722bcf302440eda707ddad06d288aeee1b4ab4 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 30 Jan 2026 22:57:05 -0800 Subject: [PATCH] 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 --- weed/admin/README.md | 16 +- weed/admin/dash/admin_server.go | 4 + weed/admin/dash/file_browser_data.go | 45 +- weed/admin/dash/file_browser_data_test.go | 24 + weed/admin/dash/s3tables_management.go | 605 ++++++++++++++++++ weed/admin/handlers/admin_handlers.go | 139 ++++ weed/admin/static/js/s3tables.js | 479 ++++++++++++++ weed/admin/view/app/file_browser.templ | 15 +- weed/admin/view/app/file_browser_templ.go | 408 ++++++------ weed/admin/view/app/object_store_users.templ | 197 +++++- .../view/app/object_store_users_templ.go | 2 +- weed/admin/view/app/s3tables_buckets.templ | 275 ++++++++ weed/admin/view/app/s3tables_buckets_templ.go | 222 +++++++ weed/admin/view/app/s3tables_namespaces.templ | 242 +++++++ .../view/app/s3tables_namespaces_templ.go | 198 ++++++ weed/admin/view/app/s3tables_tables.templ | 294 +++++++++ weed/admin/view/app/s3tables_tables_templ.go | 317 +++++++++ weed/admin/view/layout/layout.templ | 8 +- weed/admin/view/layout/layout_templ.go | 24 +- .../filer_etc/filer_etc_identity.go | 2 +- .../filer_etc/filer_etc_service_account.go | 2 +- weed/s3api/s3tables/handler.go | 66 ++ weed/s3api/s3tables/handler_bucket_create.go | 36 +- .../handler_bucket_get_list_delete.go | 42 +- weed/s3api/s3tables/handler_namespace.go | 58 +- weed/s3api/s3tables/handler_policy.go | 146 +++-- weed/s3api/s3tables/handler_table.go | 163 ++++- weed/s3api/s3tables/manager.go | 98 +++ weed/s3api/s3tables/permissions.go | 181 ++++-- weed/s3api/s3tables/permissions_test.go | 120 +++- weed/s3api/s3tables/utils.go | 75 +++ weed/s3api/tags.go | 24 +- weed/shell/command_s3tables_bucket.go | 254 ++++++++ weed/shell/command_s3tables_namespace.go | 131 ++++ weed/shell/command_s3tables_table.go | 205 ++++++ weed/shell/command_s3tables_tag.go | 131 ++++ weed/shell/s3tables_helpers.go | 89 +++ 37 files changed, 4933 insertions(+), 404 deletions(-) create mode 100644 weed/admin/dash/s3tables_management.go create mode 100644 weed/admin/static/js/s3tables.js create mode 100644 weed/admin/view/app/s3tables_buckets.templ create mode 100644 weed/admin/view/app/s3tables_buckets_templ.go create mode 100644 weed/admin/view/app/s3tables_namespaces.templ create mode 100644 weed/admin/view/app/s3tables_namespaces_templ.go create mode 100644 weed/admin/view/app/s3tables_tables.templ create mode 100644 weed/admin/view/app/s3tables_tables_templ.go create mode 100644 weed/s3api/s3tables/manager.go create mode 100644 weed/shell/command_s3tables_bucket.go create mode 100644 weed/shell/command_s3tables_namespace.go create mode 100644 weed/shell/command_s3tables_table.go create mode 100644 weed/shell/command_s3tables_tag.go create mode 100644 weed/shell/s3tables_helpers.go diff --git a/weed/admin/README.md b/weed/admin/README.md index 7b909174d..ce36f773d 100644 --- a/weed/admin/README.md +++ b/weed/admin/README.md @@ -8,6 +8,7 @@ A modern web-based administration interface for SeaweedFS clusters built with Go - **Master Management**: Monitor master nodes and leadership status - **Volume Server Management**: View volume servers, capacity, and health - **Object Store Bucket Management**: Create, delete, and manage Object Store buckets with web interface +- **S3 Tables Management**: Manage table buckets, namespaces, tables, tags, and policies via the admin UI - **System Health**: Overall cluster health monitoring - **Responsive Design**: Bootstrap-based UI that works on all devices - **Authentication**: Optional user authentication with sessions @@ -96,7 +97,6 @@ make fmt weed/admin/ ├── Makefile # Admin-specific build tasks ├── README.md # This file -├── S3_BUCKETS.md # Object Store bucket management documentation ├── admin.go # Main application entry point ├── dash/ # Server and handler logic │ ├── admin_server.go # HTTP server setup @@ -110,20 +110,20 @@ weed/admin/ ├── app/ # Application templates │ ├── admin.templ # Main dashboard template │ ├── s3_buckets.templ # Object Store bucket management template + │ ├── s3tables_*.templ # S3 Tables management templates │ └── *_templ.go # Generated Go code └── layout/ # Layout templates ├── layout.templ # Base layout template └── layout_templ.go # Generated Go code ``` -### S3 Bucket Management +### Object Store Management -The admin interface includes comprehensive Object Store bucket management capabilities. See [S3_BUCKETS.md](S3_BUCKETS.md) for detailed documentation on: +The admin interface includes Object Store and S3 Tables management capabilities: -- Creating and deleting Object Store buckets -- Viewing bucket contents and metadata -- Managing bucket permissions and settings -- API endpoints for programmatic access +- Create/delete Object Store buckets and adjust quotas or ownership. +- Manage S3 Tables buckets, namespaces, and tables. +- Update S3 Tables policies and tags via the UI and API endpoints. ## Usage @@ -276,4 +276,4 @@ The admin component follows a clean architecture: - **Business Logic**: Handler functions in `dash/` package - **Data Layer**: Communicates with SeaweedFS masters and filers -This separation makes the code maintainable and testable. \ No newline at end of file +This separation makes the code maintainable and testable. diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index f1e60252d..404227bf2 100644 --- a/weed/admin/dash/admin_server.go +++ b/weed/admin/dash/admin_server.go @@ -27,6 +27,7 @@ import ( "github.com/seaweedfs/seaweedfs/weed/s3api" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables" "github.com/seaweedfs/seaweedfs/weed/worker/tasks" _ "github.com/seaweedfs/seaweedfs/weed/credential/grpc" // Register gRPC credential store @@ -101,6 +102,8 @@ type AdminServer struct { collectionStatsCache map[string]collectionStats lastCollectionStatsUpdate time.Time collectionStatsCacheThreshold time.Duration + + s3TablesManager *s3tables.Manager } // Type definitions moved to types.go @@ -132,6 +135,7 @@ func NewAdminServer(masters string, templateFS http.FileSystem, dataDir string) filerCacheExpiration: 30 * time.Second, // Cache filers for 30 seconds configPersistence: NewConfigPersistence(dataDir), collectionStatsCacheThreshold: 30 * time.Second, + s3TablesManager: newS3TablesManager(), } // Initialize topic retention purger diff --git a/weed/admin/dash/file_browser_data.go b/weed/admin/dash/file_browser_data.go index 6e6e44c9d..d566033e7 100644 --- a/weed/admin/dash/file_browser_data.go +++ b/weed/admin/dash/file_browser_data.go @@ -39,9 +39,11 @@ type FileBrowserData struct { Breadcrumbs []BreadcrumbItem `json:"breadcrumbs"` Entries []FileEntry `json:"entries"` - LastUpdated time.Time `json:"last_updated"` - IsBucketPath bool `json:"is_bucket_path"` - BucketName string `json:"bucket_name"` + LastUpdated time.Time `json:"last_updated"` + IsBucketPath bool `json:"is_bucket_path"` + BucketName string `json:"bucket_name"` + IsTableBucketPath bool `json:"is_table_bucket_path"` + TableBucketName string `json:"table_bucket_name"` // Pagination fields PageSize int `json:"page_size"` HasNextPage bool `json:"has_next_page"` @@ -227,15 +229,28 @@ func (s *AdminServer) GetFileBrowser(dir string, lastFileName string, pageSize i } } + // Check if this is a table bucket path + isTableBucketPath := false + tableBucketName := "" + if strings.HasPrefix(dir, "/table-buckets/") { + isTableBucketPath = true + pathParts := strings.Split(strings.Trim(dir, "/"), "/") + if len(pathParts) >= 2 { + tableBucketName = pathParts[1] + } + } + return &FileBrowserData{ CurrentPath: dir, ParentPath: parentPath, Breadcrumbs: breadcrumbs, Entries: entries, - LastUpdated: time.Now(), - IsBucketPath: isBucketPath, - BucketName: bucketName, + LastUpdated: time.Now(), + IsBucketPath: isBucketPath, + BucketName: bucketName, + IsTableBucketPath: isTableBucketPath, + TableBucketName: tableBucketName, // Pagination metadata PageSize: pageSize, HasNextPage: hasNextPage, @@ -268,13 +283,17 @@ func (s *AdminServer) generateBreadcrumbs(dir string) []BreadcrumbItem { } currentPath += "/" + part - // Special handling for bucket paths - displayName := part - if len(breadcrumbs) == 1 && part == "buckets" { - displayName = "Object Store Buckets" - } else if len(breadcrumbs) == 2 && strings.HasPrefix(dir, "/buckets/") { - displayName = "📦 " + part // Add bucket icon to bucket name - } + // Special handling for bucket paths + displayName := part + if len(breadcrumbs) == 1 && part == "buckets" { + displayName = "Object Store Buckets" + } else if len(breadcrumbs) == 1 && part == "table-buckets" { + displayName = "Table Buckets" + } else if len(breadcrumbs) == 2 && strings.HasPrefix(dir, "/buckets/") { + displayName = "📦 " + part // Add bucket icon to bucket name + } else if len(breadcrumbs) == 2 && strings.HasPrefix(dir, "/table-buckets/") { + displayName = "🧊 " + part + } breadcrumbs = append(breadcrumbs, BreadcrumbItem{ Name: displayName, diff --git a/weed/admin/dash/file_browser_data_test.go b/weed/admin/dash/file_browser_data_test.go index 0605735af..e02465034 100644 --- a/weed/admin/dash/file_browser_data_test.go +++ b/weed/admin/dash/file_browser_data_test.go @@ -51,6 +51,15 @@ func TestGenerateBreadcrumbs(t *testing.T) { {Name: "📦 mybucket", Path: "/buckets/mybucket"}, }, }, + { + name: "table bucket path", + path: "/table-buckets/mytablebucket", + expected: []BreadcrumbItem{ + {Name: "Root", Path: "/"}, + {Name: "Table Buckets", Path: "/table-buckets"}, + {Name: "🧊 mytablebucket", Path: "/table-buckets/mytablebucket"}, + }, + }, { name: "bucket nested path", path: "/buckets/mybucket/folder", @@ -61,6 +70,16 @@ func TestGenerateBreadcrumbs(t *testing.T) { {Name: "folder", Path: "/buckets/mybucket/folder"}, }, }, + { + name: "table bucket nested path", + path: "/table-buckets/mytablebucket/folder", + expected: []BreadcrumbItem{ + {Name: "Root", Path: "/"}, + {Name: "Table Buckets", Path: "/table-buckets"}, + {Name: "🧊 mytablebucket", Path: "/table-buckets/mytablebucket"}, + {Name: "folder", Path: "/table-buckets/mytablebucket/folder"}, + }, + }, { name: "path with trailing slash", path: "/folder/", @@ -176,6 +195,11 @@ func TestParentPathCalculationLogic(t *testing.T) { currentDir: "/buckets/mybucket", expected: "/buckets", }, + { + name: "table bucket directory", + currentDir: "/table-buckets/mytablebucket", + expected: "/table-buckets", + }, } for _, tt := range tests { diff --git a/weed/admin/dash/s3tables_management.go b/weed/admin/dash/s3tables_management.go new file mode 100644 index 000000000..bf9bd7c98 --- /dev/null +++ b/weed/admin/dash/s3tables_management.go @@ -0,0 +1,605 @@ +package dash + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/seaweedfs/seaweedfs/weed/glog" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables" +) + +// S3Tables data structures for admin UI + +type S3TablesBucketsData struct { + Username string `json:"username"` + Buckets []S3TablesBucketSummary `json:"buckets"` + TotalBuckets int `json:"total_buckets"` + LastUpdated time.Time `json:"last_updated"` +} + +type S3TablesBucketSummary struct { + ARN string `json:"arn"` + Name string `json:"name"` + OwnerAccountID string `json:"ownerAccountId"` + CreatedAt time.Time `json:"createdAt"` +} + +type S3TablesNamespacesData struct { + Username string `json:"username"` + BucketARN string `json:"bucket_arn"` + Namespaces []s3tables.NamespaceSummary `json:"namespaces"` + TotalNamespaces int `json:"total_namespaces"` + LastUpdated time.Time `json:"last_updated"` +} + +type S3TablesTablesData struct { + Username string `json:"username"` + BucketARN string `json:"bucket_arn"` + Namespace string `json:"namespace"` + Tables []s3tables.TableSummary `json:"tables"` + TotalTables int `json:"total_tables"` + LastUpdated time.Time `json:"last_updated"` +} + +type tableBucketMetadata struct { + Name string `json:"name"` + CreatedAt time.Time `json:"createdAt"` + OwnerAccountID string `json:"ownerAccountId"` +} + +// S3Tables manager helpers + +const s3TablesAdminListLimit = 1000 + +func newS3TablesManager() *s3tables.Manager { + manager := s3tables.NewManager() + manager.SetAccountID(s3_constants.AccountAdminId) + return manager +} + +func (s *AdminServer) executeS3TablesOperation(ctx context.Context, operation string, req interface{}, resp interface{}) error { + return s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + mgrClient := s3tables.NewManagerClient(client) + return s.s3TablesManager.Execute(ctx, mgrClient, operation, req, resp, s3_constants.AccountAdminId) + }) +} + +// S3Tables data retrieval for pages + +func (s *AdminServer) GetS3TablesBucketsData(ctx context.Context) (S3TablesBucketsData, error) { + var buckets []S3TablesBucketSummary + err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + resp, err := client.ListEntries(ctx, &filer_pb.ListEntriesRequest{ + Directory: s3tables.TablesPath, + Limit: uint32(s3TablesAdminListLimit * 2), + InclusiveStartFrom: true, + }) + if err != nil { + return err + } + for len(buckets) < s3TablesAdminListLimit { + entry, recvErr := resp.Recv() + if recvErr != nil { + if recvErr == io.EOF { + break + } + return recvErr + } + if entry.Entry == nil || !entry.Entry.IsDirectory { + continue + } + if strings.HasPrefix(entry.Entry.Name, ".") { + continue + } + metaBytes, ok := entry.Entry.Extended[s3tables.ExtendedKeyMetadata] + if !ok { + continue + } + var metadata tableBucketMetadata + if err := json.Unmarshal(metaBytes, &metadata); err != nil { + glog.V(1).Infof("S3Tables: failed to decode table bucket metadata for %s: %v", entry.Entry.Name, err) + continue + } + arn, err := s3tables.BuildBucketARN(s3tables.DefaultRegion, metadata.OwnerAccountID, entry.Entry.Name) + if err != nil { + glog.V(1).Infof("S3Tables: failed to build table bucket ARN for %s: %v", entry.Entry.Name, err) + continue + } + buckets = append(buckets, S3TablesBucketSummary{ + ARN: arn, + Name: entry.Entry.Name, + OwnerAccountID: metadata.OwnerAccountID, + CreatedAt: metadata.CreatedAt, + }) + } + return nil + }) + if err != nil { + return S3TablesBucketsData{}, err + } + return S3TablesBucketsData{ + Buckets: buckets, + TotalBuckets: len(buckets), + LastUpdated: time.Now(), + }, nil +} + +func (s *AdminServer) GetS3TablesNamespacesData(ctx context.Context, bucketArn string) (S3TablesNamespacesData, error) { + var resp s3tables.ListNamespacesResponse + req := &s3tables.ListNamespacesRequest{TableBucketARN: bucketArn, MaxNamespaces: s3TablesAdminListLimit} + if err := s.executeS3TablesOperation(ctx, "ListNamespaces", req, &resp); err != nil { + return S3TablesNamespacesData{}, err + } + return S3TablesNamespacesData{ + BucketARN: bucketArn, + Namespaces: resp.Namespaces, + TotalNamespaces: len(resp.Namespaces), + LastUpdated: time.Now(), + }, nil +} + +func (s *AdminServer) GetS3TablesTablesData(ctx context.Context, bucketArn, namespace string) (S3TablesTablesData, error) { + var resp s3tables.ListTablesResponse + var ns []string + if namespace != "" { + ns = []string{namespace} + } + req := &s3tables.ListTablesRequest{TableBucketARN: bucketArn, Namespace: ns, MaxTables: s3TablesAdminListLimit} + if err := s.executeS3TablesOperation(ctx, "ListTables", req, &resp); err != nil { + return S3TablesTablesData{}, err + } + return S3TablesTablesData{ + BucketARN: bucketArn, + Namespace: namespace, + Tables: resp.Tables, + TotalTables: len(resp.Tables), + LastUpdated: time.Now(), + }, nil +} + +// API handlers + +func (s *AdminServer) ListS3TablesBucketsAPI(c *gin.Context) { + data, err := s.GetS3TablesBucketsData(c.Request.Context()) + if err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(200, data) +} + +func (s *AdminServer) CreateS3TablesBucket(c *gin.Context) { + var req struct { + Name string `json:"name"` + Tags map[string]string `json:"tags"` + Owner string `json:"owner"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + if req.Name == "" { + c.JSON(400, gin.H{"error": "Bucket name is required"}) + return + } + owner := strings.TrimSpace(req.Owner) + if len(owner) > MaxOwnerNameLength { + c.JSON(400, gin.H{"error": fmt.Sprintf("Owner name must be %d characters or less", MaxOwnerNameLength)}) + return + } + if len(req.Tags) > 0 { + if err := s3tables.ValidateTags(req.Tags); err != nil { + c.JSON(400, gin.H{"error": "Invalid tags: " + err.Error()}) + return + } + } + createReq := &s3tables.CreateTableBucketRequest{Name: req.Name, Tags: req.Tags} + var resp s3tables.CreateTableBucketResponse + if err := s.executeS3TablesOperation(c.Request.Context(), "CreateTableBucket", createReq, &resp); err != nil { + writeS3TablesError(c, err) + return + } + if owner != "" { + if err := s.SetTableBucketOwner(c.Request.Context(), req.Name, owner); err != nil { + deleteReq := &s3tables.DeleteTableBucketRequest{TableBucketARN: resp.ARN} + if deleteErr := s.executeS3TablesOperation(c.Request.Context(), "DeleteTableBucket", deleteReq, nil); deleteErr != nil { + c.JSON(500, gin.H{"error": fmt.Sprintf("Failed to set table bucket owner: %v; rollback delete failed: %v", err, deleteErr)}) + return + } + writeS3TablesError(c, err) + return + } + } + c.JSON(201, gin.H{"arn": resp.ARN}) +} + +func (s *AdminServer) SetTableBucketOwner(ctx context.Context, bucketName, owner string) error { + return s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + resp, err := client.LookupDirectoryEntry(ctx, &filer_pb.LookupDirectoryEntryRequest{ + Directory: s3tables.TablesPath, + Name: bucketName, + }) + if err != nil { + return fmt.Errorf("lookup table bucket %s: %w", bucketName, err) + } + if resp.Entry == nil { + return fmt.Errorf("table bucket %s not found", bucketName) + } + entry := resp.Entry + if entry.Extended == nil { + return fmt.Errorf("table bucket %s metadata missing", bucketName) + } + metaBytes, ok := entry.Extended[s3tables.ExtendedKeyMetadata] + if !ok { + return fmt.Errorf("table bucket %s metadata missing", bucketName) + } + var metadata tableBucketMetadata + if err := json.Unmarshal(metaBytes, &metadata); err != nil { + return fmt.Errorf("failed to parse table bucket metadata: %w", err) + } + metadata.OwnerAccountID = owner + updated, err := json.Marshal(&metadata) + if err != nil { + return fmt.Errorf("failed to marshal table bucket metadata: %w", err) + } + entry.Extended[s3tables.ExtendedKeyMetadata] = updated + if _, err := client.UpdateEntry(ctx, &filer_pb.UpdateEntryRequest{ + Directory: s3tables.TablesPath, + Entry: entry, + }); err != nil { + return fmt.Errorf("failed to update table bucket owner: %w", err) + } + return nil + }) +} + +func (s *AdminServer) DeleteS3TablesBucket(c *gin.Context) { + bucketArn := c.Query("bucket") + if bucketArn == "" { + c.JSON(400, gin.H{"error": "Bucket ARN is required"}) + return + } + req := &s3tables.DeleteTableBucketRequest{TableBucketARN: bucketArn} + if err := s.executeS3TablesOperation(c.Request.Context(), "DeleteTableBucket", req, nil); err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(200, gin.H{"message": "Bucket deleted"}) +} + +func (s *AdminServer) ListS3TablesNamespacesAPI(c *gin.Context) { + bucketArn := c.Query("bucket") + if bucketArn == "" { + c.JSON(400, gin.H{"error": "bucket query parameter is required"}) + return + } + data, err := s.GetS3TablesNamespacesData(c.Request.Context(), bucketArn) + if err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(200, data) +} + +func (s *AdminServer) CreateS3TablesNamespace(c *gin.Context) { + var req struct { + BucketARN string `json:"bucket_arn"` + Name string `json:"name"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + if req.BucketARN == "" || req.Name == "" { + c.JSON(400, gin.H{"error": "bucket_arn and name are required"}) + return + } + createReq := &s3tables.CreateNamespaceRequest{TableBucketARN: req.BucketARN, Namespace: []string{req.Name}} + var resp s3tables.CreateNamespaceResponse + if err := s.executeS3TablesOperation(c.Request.Context(), "CreateNamespace", createReq, &resp); err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(201, gin.H{"namespace": resp.Namespace}) +} + +func (s *AdminServer) DeleteS3TablesNamespace(c *gin.Context) { + bucketArn := c.Query("bucket") + namespace := c.Query("name") + if bucketArn == "" || namespace == "" { + c.JSON(400, gin.H{"error": "bucket and name query parameters are required"}) + return + } + req := &s3tables.DeleteNamespaceRequest{TableBucketARN: bucketArn, Namespace: []string{namespace}} + if err := s.executeS3TablesOperation(c.Request.Context(), "DeleteNamespace", req, nil); err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(200, gin.H{"message": "Namespace deleted"}) +} + +func (s *AdminServer) ListS3TablesTablesAPI(c *gin.Context) { + bucketArn := c.Query("bucket") + if bucketArn == "" { + c.JSON(400, gin.H{"error": "bucket query parameter is required"}) + return + } + namespace := c.Query("namespace") + data, err := s.GetS3TablesTablesData(c.Request.Context(), bucketArn, namespace) + if err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(200, data) +} + +func (s *AdminServer) CreateS3TablesTable(c *gin.Context) { + var req struct { + BucketARN string `json:"bucket_arn"` + Namespace string `json:"namespace"` + Name string `json:"name"` + Format string `json:"format"` + Tags map[string]string `json:"tags"` + Metadata *s3tables.TableMetadata `json:"metadata"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + if req.BucketARN == "" || req.Namespace == "" || req.Name == "" { + c.JSON(400, gin.H{"error": "bucket_arn, namespace, and name are required"}) + return + } + format := req.Format + if format == "" { + format = "ICEBERG" + } + if len(req.Tags) > 0 { + if err := s3tables.ValidateTags(req.Tags); err != nil { + c.JSON(400, gin.H{"error": "Invalid tags: " + err.Error()}) + return + } + } + createReq := &s3tables.CreateTableRequest{ + TableBucketARN: req.BucketARN, + Namespace: []string{req.Namespace}, + Name: req.Name, + Format: format, + Tags: req.Tags, + Metadata: req.Metadata, + } + var resp s3tables.CreateTableResponse + if err := s.executeS3TablesOperation(c.Request.Context(), "CreateTable", createReq, &resp); err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(201, gin.H{"table_arn": resp.TableARN, "version_token": resp.VersionToken}) +} + +func (s *AdminServer) DeleteS3TablesTable(c *gin.Context) { + bucketArn := c.Query("bucket") + namespace := c.Query("namespace") + name := c.Query("name") + version := c.Query("version") + if bucketArn == "" || namespace == "" || name == "" { + c.JSON(400, gin.H{"error": "bucket, namespace, and name query parameters are required"}) + return + } + req := &s3tables.DeleteTableRequest{TableBucketARN: bucketArn, Namespace: []string{namespace}, Name: name, VersionToken: version} + if err := s.executeS3TablesOperation(c.Request.Context(), "DeleteTable", req, nil); err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(200, gin.H{"message": "Table deleted"}) +} + +func (s *AdminServer) PutS3TablesBucketPolicy(c *gin.Context) { + var req struct { + BucketARN string `json:"bucket_arn"` + Policy string `json:"policy"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + if req.BucketARN == "" || req.Policy == "" { + c.JSON(400, gin.H{"error": "bucket_arn and policy are required"}) + return + } + putReq := &s3tables.PutTableBucketPolicyRequest{TableBucketARN: req.BucketARN, ResourcePolicy: req.Policy} + if err := s.executeS3TablesOperation(c.Request.Context(), "PutTableBucketPolicy", putReq, nil); err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(200, gin.H{"message": "Policy updated"}) +} + +func (s *AdminServer) GetS3TablesBucketPolicy(c *gin.Context) { + bucketArn := c.Query("bucket") + if bucketArn == "" { + c.JSON(400, gin.H{"error": "bucket query parameter is required"}) + return + } + getReq := &s3tables.GetTableBucketPolicyRequest{TableBucketARN: bucketArn} + var resp s3tables.GetTableBucketPolicyResponse + if err := s.executeS3TablesOperation(c.Request.Context(), "GetTableBucketPolicy", getReq, &resp); err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(200, gin.H{"policy": resp.ResourcePolicy}) +} + +func (s *AdminServer) DeleteS3TablesBucketPolicy(c *gin.Context) { + bucketArn := c.Query("bucket") + if bucketArn == "" { + c.JSON(400, gin.H{"error": "bucket query parameter is required"}) + return + } + deleteReq := &s3tables.DeleteTableBucketPolicyRequest{TableBucketARN: bucketArn} + if err := s.executeS3TablesOperation(c.Request.Context(), "DeleteTableBucketPolicy", deleteReq, nil); err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(200, gin.H{"message": "Policy deleted"}) +} + +func (s *AdminServer) PutS3TablesTablePolicy(c *gin.Context) { + var req struct { + BucketARN string `json:"bucket_arn"` + Namespace string `json:"namespace"` + Name string `json:"name"` + Policy string `json:"policy"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + if req.BucketARN == "" || req.Namespace == "" || req.Name == "" || req.Policy == "" { + c.JSON(400, gin.H{"error": "bucket_arn, namespace, name, and policy are required"}) + return + } + putReq := &s3tables.PutTablePolicyRequest{TableBucketARN: req.BucketARN, Namespace: []string{req.Namespace}, Name: req.Name, ResourcePolicy: req.Policy} + if err := s.executeS3TablesOperation(c.Request.Context(), "PutTablePolicy", putReq, nil); err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(200, gin.H{"message": "Policy updated"}) +} + +func (s *AdminServer) GetS3TablesTablePolicy(c *gin.Context) { + bucketArn := c.Query("bucket") + namespace := c.Query("namespace") + name := c.Query("name") + if bucketArn == "" || namespace == "" || name == "" { + c.JSON(400, gin.H{"error": "bucket, namespace, and name query parameters are required"}) + return + } + getReq := &s3tables.GetTablePolicyRequest{TableBucketARN: bucketArn, Namespace: []string{namespace}, Name: name} + var resp s3tables.GetTablePolicyResponse + if err := s.executeS3TablesOperation(c.Request.Context(), "GetTablePolicy", getReq, &resp); err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(200, gin.H{"policy": resp.ResourcePolicy}) +} + +func (s *AdminServer) DeleteS3TablesTablePolicy(c *gin.Context) { + bucketArn := c.Query("bucket") + namespace := c.Query("namespace") + name := c.Query("name") + if bucketArn == "" || namespace == "" || name == "" { + c.JSON(400, gin.H{"error": "bucket, namespace, and name query parameters are required"}) + return + } + deleteReq := &s3tables.DeleteTablePolicyRequest{TableBucketARN: bucketArn, Namespace: []string{namespace}, Name: name} + if err := s.executeS3TablesOperation(c.Request.Context(), "DeleteTablePolicy", deleteReq, nil); err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(200, gin.H{"message": "Policy deleted"}) +} + +func (s *AdminServer) TagS3TablesResource(c *gin.Context) { + var req struct { + ResourceARN string `json:"resource_arn"` + Tags map[string]string `json:"tags"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + if req.ResourceARN == "" || len(req.Tags) == 0 { + c.JSON(400, gin.H{"error": "resource_arn and tags are required"}) + return + } + if err := s3tables.ValidateTags(req.Tags); err != nil { + c.JSON(400, gin.H{"error": "Invalid tags: " + err.Error()}) + return + } + tagReq := &s3tables.TagResourceRequest{ResourceARN: req.ResourceARN, Tags: req.Tags} + if err := s.executeS3TablesOperation(c.Request.Context(), "TagResource", tagReq, nil); err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(200, gin.H{"message": "Tags updated"}) +} + +func (s *AdminServer) ListS3TablesTags(c *gin.Context) { + resourceArn := c.Query("arn") + if resourceArn == "" { + c.JSON(400, gin.H{"error": "arn query parameter is required"}) + return + } + listReq := &s3tables.ListTagsForResourceRequest{ResourceARN: resourceArn} + var resp s3tables.ListTagsForResourceResponse + if err := s.executeS3TablesOperation(c.Request.Context(), "ListTagsForResource", listReq, &resp); err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(200, resp) +} + +func (s *AdminServer) UntagS3TablesResource(c *gin.Context) { + var req struct { + ResourceARN string `json:"resource_arn"` + TagKeys []string `json:"tag_keys"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + if req.ResourceARN == "" || len(req.TagKeys) == 0 { + c.JSON(400, gin.H{"error": "resource_arn and tag_keys are required"}) + return + } + untagReq := &s3tables.UntagResourceRequest{ResourceARN: req.ResourceARN, TagKeys: req.TagKeys} + if err := s.executeS3TablesOperation(c.Request.Context(), "UntagResource", untagReq, nil); err != nil { + writeS3TablesError(c, err) + return + } + c.JSON(200, gin.H{"message": "Tags removed"}) +} + +func parseS3TablesErrorMessage(err error) string { + if err == nil { + return "" + } + var s3Err *s3tables.S3TablesError + if errors.As(err, &s3Err) { + if s3Err.Message != "" { + return fmt.Sprintf("%s: %s", s3Err.Type, s3Err.Message) + } + return s3Err.Type + } + return err.Error() +} + +func writeS3TablesError(c *gin.Context, err error) { + c.JSON(s3TablesErrorStatus(err), gin.H{"error": parseS3TablesErrorMessage(err)}) +} + +func s3TablesErrorStatus(err error) int { + var s3Err *s3tables.S3TablesError + if errors.As(err, &s3Err) { + switch s3Err.Type { + case s3tables.ErrCodeInvalidRequest: + return http.StatusBadRequest + case s3tables.ErrCodeNoSuchBucket, s3tables.ErrCodeNoSuchNamespace, s3tables.ErrCodeNoSuchTable, s3tables.ErrCodeNoSuchPolicy: + return http.StatusNotFound + case s3tables.ErrCodeAccessDenied: + return http.StatusForbidden + case s3tables.ErrCodeBucketAlreadyExists, s3tables.ErrCodeNamespaceAlreadyExists, s3tables.ErrCodeTableAlreadyExists, s3tables.ErrCodeConflict: + return http.StatusConflict + } + } + return http.StatusInternalServerError +} diff --git a/weed/admin/handlers/admin_handlers.go b/weed/admin/handlers/admin_handlers.go index 52d8f9f66..3bc61864d 100644 --- a/weed/admin/handlers/admin_handlers.go +++ b/weed/admin/handlers/admin_handlers.go @@ -9,6 +9,8 @@ import ( "github.com/seaweedfs/seaweedfs/weed/admin/dash" "github.com/seaweedfs/seaweedfs/weed/admin/view/app" "github.com/seaweedfs/seaweedfs/weed/admin/view/layout" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables" "github.com/seaweedfs/seaweedfs/weed/stats" ) @@ -86,6 +88,9 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser, protected.GET("/object-store/users", h.userHandlers.ShowObjectStoreUsers) protected.GET("/object-store/policies", h.policyHandlers.ShowPolicies) protected.GET("/object-store/service-accounts", h.serviceAccountHandlers.ShowServiceAccounts) + protected.GET("/object-store/s3tables/buckets", h.ShowS3TablesBuckets) + protected.GET("/object-store/s3tables/buckets/:bucket/namespaces", h.ShowS3TablesNamespaces) + protected.GET("/object-store/s3tables/buckets/:bucket/namespaces/:namespace/tables", h.ShowS3TablesTables) // File browser routes protected.GET("/files", h.fileBrowserHandlers.ShowFileBrowser) @@ -174,6 +179,29 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser, objectStorePoliciesApi.POST("/validate", h.policyHandlers.ValidatePolicy) } + // S3 Tables API routes + s3TablesApi := api.Group("/s3tables") + { + s3TablesApi.GET("/buckets", h.adminServer.ListS3TablesBucketsAPI) + s3TablesApi.POST("/buckets", dash.RequireWriteAccess(), h.adminServer.CreateS3TablesBucket) + s3TablesApi.DELETE("/buckets", dash.RequireWriteAccess(), h.adminServer.DeleteS3TablesBucket) + s3TablesApi.GET("/namespaces", h.adminServer.ListS3TablesNamespacesAPI) + s3TablesApi.POST("/namespaces", dash.RequireWriteAccess(), h.adminServer.CreateS3TablesNamespace) + s3TablesApi.DELETE("/namespaces", dash.RequireWriteAccess(), h.adminServer.DeleteS3TablesNamespace) + s3TablesApi.GET("/tables", h.adminServer.ListS3TablesTablesAPI) + s3TablesApi.POST("/tables", dash.RequireWriteAccess(), h.adminServer.CreateS3TablesTable) + s3TablesApi.DELETE("/tables", dash.RequireWriteAccess(), h.adminServer.DeleteS3TablesTable) + s3TablesApi.PUT("/bucket-policy", dash.RequireWriteAccess(), h.adminServer.PutS3TablesBucketPolicy) + s3TablesApi.GET("/bucket-policy", h.adminServer.GetS3TablesBucketPolicy) + s3TablesApi.DELETE("/bucket-policy", dash.RequireWriteAccess(), h.adminServer.DeleteS3TablesBucketPolicy) + s3TablesApi.PUT("/table-policy", dash.RequireWriteAccess(), h.adminServer.PutS3TablesTablePolicy) + s3TablesApi.GET("/table-policy", h.adminServer.GetS3TablesTablePolicy) + s3TablesApi.DELETE("/table-policy", dash.RequireWriteAccess(), h.adminServer.DeleteS3TablesTablePolicy) + s3TablesApi.PUT("/tags", dash.RequireWriteAccess(), h.adminServer.TagS3TablesResource) + s3TablesApi.GET("/tags", h.adminServer.ListS3TablesTags) + s3TablesApi.DELETE("/tags", dash.RequireWriteAccess(), h.adminServer.UntagS3TablesResource) + } + // File management API routes filesApi := api.Group("/files") { @@ -228,6 +256,9 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser, r.GET("/object-store/users", h.userHandlers.ShowObjectStoreUsers) r.GET("/object-store/policies", h.policyHandlers.ShowPolicies) r.GET("/object-store/service-accounts", h.serviceAccountHandlers.ShowServiceAccounts) + r.GET("/object-store/s3tables/buckets", h.ShowS3TablesBuckets) + r.GET("/object-store/s3tables/buckets/:bucket/namespaces", h.ShowS3TablesNamespaces) + r.GET("/object-store/s3tables/buckets/:bucket/namespaces/:namespace/tables", h.ShowS3TablesTables) // File browser routes r.GET("/files", h.fileBrowserHandlers.ShowFileBrowser) @@ -315,6 +346,29 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser, objectStorePoliciesApi.POST("/validate", h.policyHandlers.ValidatePolicy) } + // S3 Tables API routes + s3TablesApi := api.Group("/s3tables") + { + s3TablesApi.GET("/buckets", h.adminServer.ListS3TablesBucketsAPI) + s3TablesApi.POST("/buckets", h.adminServer.CreateS3TablesBucket) + s3TablesApi.DELETE("/buckets", h.adminServer.DeleteS3TablesBucket) + s3TablesApi.GET("/namespaces", h.adminServer.ListS3TablesNamespacesAPI) + s3TablesApi.POST("/namespaces", h.adminServer.CreateS3TablesNamespace) + s3TablesApi.DELETE("/namespaces", h.adminServer.DeleteS3TablesNamespace) + s3TablesApi.GET("/tables", h.adminServer.ListS3TablesTablesAPI) + s3TablesApi.POST("/tables", h.adminServer.CreateS3TablesTable) + s3TablesApi.DELETE("/tables", h.adminServer.DeleteS3TablesTable) + s3TablesApi.PUT("/bucket-policy", h.adminServer.PutS3TablesBucketPolicy) + s3TablesApi.GET("/bucket-policy", h.adminServer.GetS3TablesBucketPolicy) + s3TablesApi.DELETE("/bucket-policy", h.adminServer.DeleteS3TablesBucketPolicy) + s3TablesApi.PUT("/table-policy", h.adminServer.PutS3TablesTablePolicy) + s3TablesApi.GET("/table-policy", h.adminServer.GetS3TablesTablePolicy) + s3TablesApi.DELETE("/table-policy", h.adminServer.DeleteS3TablesTablePolicy) + s3TablesApi.PUT("/tags", h.adminServer.TagS3TablesResource) + s3TablesApi.GET("/tags", h.adminServer.ListS3TablesTags) + s3TablesApi.DELETE("/tags", h.adminServer.UntagS3TablesResource) + } + // File management API routes filesApi := api.Group("/files") { @@ -398,6 +452,91 @@ func (h *AdminHandlers) ShowS3Buckets(c *gin.Context) { } } +// ShowS3TablesBuckets renders the S3 Tables buckets page +func (h *AdminHandlers) ShowS3TablesBuckets(c *gin.Context) { + username := c.GetString("username") + if username == "" { + username = "admin" + } + + data, err := h.adminServer.GetS3TablesBucketsData(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get S3 Tables buckets: " + err.Error()}) + return + } + data.Username = username + + c.Header("Content-Type", "text/html") + component := app.S3TablesBuckets(data) + layoutComponent := layout.Layout(c, component) + if err := layoutComponent.Render(c.Request.Context(), c.Writer); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) + } +} + +// ShowS3TablesNamespaces renders namespaces for a table bucket +func (h *AdminHandlers) ShowS3TablesNamespaces(c *gin.Context) { + username := c.GetString("username") + if username == "" { + username = "admin" + } + + bucketName := c.Param("bucket") + arn, err := buildS3TablesBucketArn(bucketName) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + data, err := h.adminServer.GetS3TablesNamespacesData(c.Request.Context(), arn) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get S3 Tables namespaces: " + err.Error()}) + return + } + data.Username = username + + c.Header("Content-Type", "text/html") + component := app.S3TablesNamespaces(data) + layoutComponent := layout.Layout(c, component) + if err := layoutComponent.Render(c.Request.Context(), c.Writer); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) + } +} + +// ShowS3TablesTables renders tables for a namespace +func (h *AdminHandlers) ShowS3TablesTables(c *gin.Context) { + username := c.GetString("username") + if username == "" { + username = "admin" + } + + bucketName := c.Param("bucket") + namespace := c.Param("namespace") + arn, err := buildS3TablesBucketArn(bucketName) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + data, err := h.adminServer.GetS3TablesTablesData(c.Request.Context(), arn, namespace) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get S3 Tables tables: " + err.Error()}) + return + } + data.Username = username + + c.Header("Content-Type", "text/html") + component := app.S3TablesTables(data) + layoutComponent := layout.Layout(c, component) + if err := layoutComponent.Render(c.Request.Context(), c.Writer); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) + } +} + +func buildS3TablesBucketArn(bucketName string) (string, error) { + return s3tables.BuildBucketARN(s3tables.DefaultRegion, s3_constants.AccountAdminId, bucketName) +} + // ShowBucketDetails returns detailed information about a specific bucket func (h *AdminHandlers) ShowBucketDetails(c *gin.Context) { bucketName := c.Param("bucket") diff --git a/weed/admin/static/js/s3tables.js b/weed/admin/static/js/s3tables.js new file mode 100644 index 000000000..4c484bce5 --- /dev/null +++ b/weed/admin/static/js/s3tables.js @@ -0,0 +1,479 @@ +/** + * Shared S3 Tables functionality for the SeaweedFS Admin Dashboard. + */ + +// Shared Modals +let s3tablesBucketDeleteModal = null; +let s3tablesBucketPolicyModal = null; +let s3tablesNamespaceDeleteModal = null; +let s3tablesTableDeleteModal = null; +let s3tablesTablePolicyModal = null; +let s3tablesTagsModal = null; + +/** + * Initialize S3 Tables Buckets Page + */ +function initS3TablesBuckets() { + s3tablesBucketDeleteModal = new bootstrap.Modal(document.getElementById('deleteS3TablesBucketModal')); + s3tablesBucketPolicyModal = new bootstrap.Modal(document.getElementById('s3tablesBucketPolicyModal')); + s3tablesTagsModal = new bootstrap.Modal(document.getElementById('s3tablesTagsModal')); + + const ownerSelect = document.getElementById('s3tablesBucketOwner'); + if (ownerSelect) { + document.getElementById('createS3TablesBucketModal').addEventListener('show.bs.modal', async function () { + if (ownerSelect.options.length <= 1) { + try { + const response = await fetch('/api/users'); + const data = await response.json(); + const users = data.users || []; + users.forEach(user => { + const option = document.createElement('option'); + option.value = user.username; + option.textContent = user.username; + ownerSelect.appendChild(option); + }); + } catch (error) { + console.error('Error fetching users for owner dropdown:', error); + ownerSelect.innerHTML = ''; + ownerSelect.selectedIndex = 0; + } + } + }); + } + + document.querySelectorAll('.s3tables-delete-bucket-btn').forEach(button => { + button.addEventListener('click', function () { + document.getElementById('deleteS3TablesBucketName').textContent = this.dataset.bucketName || ''; + document.getElementById('deleteS3TablesBucketModal').dataset.bucketArn = this.dataset.bucketArn || ''; + s3tablesBucketDeleteModal.show(); + }); + }); + + document.querySelectorAll('.s3tables-bucket-policy-btn').forEach(button => { + button.addEventListener('click', function () { + const bucketArn = this.dataset.bucketArn || ''; + document.getElementById('s3tablesBucketPolicyArn').value = bucketArn; + loadS3TablesBucketPolicy(bucketArn); + s3tablesBucketPolicyModal.show(); + }); + }); + + document.querySelectorAll('.s3tables-tags-btn').forEach(button => { + button.addEventListener('click', function () { + const resourceArn = this.dataset.resourceArn || ''; + openS3TablesTags(resourceArn); + }); + }); + + const createForm = document.getElementById('createS3TablesBucketForm'); + if (createForm) { + createForm.addEventListener('submit', async function (e) { + e.preventDefault(); + const name = document.getElementById('s3tablesBucketName').value.trim(); + const owner = ownerSelect.value; + const tagsInput = document.getElementById('s3tablesBucketTags').value.trim(); + const tags = parseTagsInput(tagsInput); + if (tags === null) return; + const payload = { name: name, tags: tags, owner: owner }; + + try { + const response = await fetch('/api/s3tables/buckets', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + const data = await response.json(); + if (!response.ok) { + alert(data.error || 'Failed to create bucket'); + return; + } + alert('Bucket created successfully'); + location.reload(); + } catch (error) { + alert('Failed to create bucket: ' + error.message); + } + }); + } + + const policyForm = document.getElementById('s3tablesBucketPolicyForm'); + if (policyForm) { + policyForm.addEventListener('submit', async function (e) { + e.preventDefault(); + const bucketArn = document.getElementById('s3tablesBucketPolicyArn').value; + const policy = document.getElementById('s3tablesBucketPolicyText').value.trim(); + if (!policy) { + alert('Policy JSON is required'); + return; + } + try { + const response = await fetch('/api/s3tables/bucket-policy', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ bucket_arn: bucketArn, policy: policy }) + }); + const data = await response.json(); + if (!response.ok) { + alert(data.error || 'Failed to update policy'); + return; + } + alert('Policy updated'); + s3tablesBucketPolicyModal.hide(); + } catch (error) { + alert('Failed to update policy: ' + error.message); + } + }); + } + + const tagsForm = document.getElementById('s3tablesTagsForm'); + if (tagsForm) { + tagsForm.addEventListener('submit', async function (e) { + e.preventDefault(); + const resourceArn = document.getElementById('s3tablesTagsResourceArn').value; + const tags = parseTagsInput(document.getElementById('s3tablesTagsInput').value.trim()); + if (tags === null || Object.keys(tags).length === 0) { + alert('Please provide tags to update'); + return; + } + await updateS3TablesTags(resourceArn, tags); + }); + } +} + +/** + * Initialize S3 Tables Tables Page + */ +function initS3TablesTables() { + s3tablesTableDeleteModal = new bootstrap.Modal(document.getElementById('deleteS3TablesTableModal')); + s3tablesTablePolicyModal = new bootstrap.Modal(document.getElementById('s3tablesTablePolicyModal')); + s3tablesTagsModal = new bootstrap.Modal(document.getElementById('s3tablesTagsModal')); + + const dataContainer = document.getElementById('s3tables-tables-content'); + const dataBucketArn = dataContainer.dataset.bucketArn || ''; + const dataNamespace = dataContainer.dataset.namespace || ''; + + document.querySelectorAll('.s3tables-delete-table-btn').forEach(button => { + button.addEventListener('click', function () { + document.getElementById('deleteS3TablesTableName').textContent = this.dataset.tableName || ''; + document.getElementById('deleteS3TablesTableModal').dataset.tableName = this.dataset.tableName || ''; + s3tablesTableDeleteModal.show(); + }); + }); + + document.querySelectorAll('.s3tables-table-policy-btn').forEach(button => { + button.addEventListener('click', function () { + document.getElementById('s3tablesTablePolicyBucketArn').value = dataBucketArn; + document.getElementById('s3tablesTablePolicyNamespace').value = dataNamespace; + document.getElementById('s3tablesTablePolicyName').value = this.dataset.tableName || ''; + loadS3TablesTablePolicy(dataBucketArn, dataNamespace, this.dataset.tableName || ''); + s3tablesTablePolicyModal.show(); + }); + }); + + document.querySelectorAll('.s3tables-tags-btn').forEach(button => { + button.addEventListener('click', function () { + const resourceArn = this.dataset.resourceArn || ''; + openS3TablesTags(resourceArn); + }); + }); + + const createForm = document.getElementById('createS3TablesTableForm'); + if (createForm) { + createForm.addEventListener('submit', async function (e) { + e.preventDefault(); + const name = document.getElementById('s3tablesTableName').value.trim(); + const format = document.getElementById('s3tablesTableFormat').value; + const metadataText = document.getElementById('s3tablesTableMetadata').value.trim(); + const tagsInput = document.getElementById('s3tablesTableTags').value.trim(); + const tags = parseTagsInput(tagsInput); + if (tags === null) return; + let metadata = null; + if (metadataText) { + try { + metadata = JSON.parse(metadataText); + } catch (error) { + alert('Invalid metadata JSON'); + return; + } + } + const payload = { bucket_arn: dataBucketArn, namespace: dataNamespace, name: name, format: format, tags: tags }; + if (metadata) { + payload.metadata = metadata; + } + try { + const response = await fetch('/api/s3tables/tables', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + const data = await response.json(); + if (!response.ok) { + alert(data.error || 'Failed to create table'); + return; + } + alert('Table created'); + location.reload(); + } catch (error) { + alert('Failed to create table: ' + error.message); + } + }); + } + + const policyForm = document.getElementById('s3tablesTablePolicyForm'); + if (policyForm) { + policyForm.addEventListener('submit', async function (e) { + e.preventDefault(); + const policy = document.getElementById('s3tablesTablePolicyText').value.trim(); + if (!policy) { + alert('Policy JSON is required'); + return; + } + try { + const response = await fetch('/api/s3tables/table-policy', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ bucket_arn: dataBucketArn, namespace: dataNamespace, name: document.getElementById('s3tablesTablePolicyName').value, policy: policy }) + }); + const data = await response.json(); + if (!response.ok) { + alert(data.error || 'Failed to update policy'); + return; + } + alert('Policy updated'); + s3tablesTablePolicyModal.hide(); + } catch (error) { + alert('Failed to update policy: ' + error.message); + } + }); + } + + const tagsForm = document.getElementById('s3tablesTagsForm'); + if (tagsForm) { + tagsForm.addEventListener('submit', async function (e) { + e.preventDefault(); + const resourceArn = document.getElementById('s3tablesTagsResourceArn').value; + const tags = parseTagsInput(document.getElementById('s3tablesTagsInput').value.trim()); + if (tags === null || Object.keys(tags).length === 0) { + alert('Please provide tags to update'); + return; + } + await updateS3TablesTags(resourceArn, tags); + }); + } +} + +// Global scope functions used by onclick handlers + +async function deleteS3TablesBucket() { + const bucketArn = document.getElementById('deleteS3TablesBucketModal').dataset.bucketArn; + if (!bucketArn) return; + try { + const response = await fetch(`/api/s3tables/buckets?bucket=${encodeURIComponent(bucketArn)}`, { method: 'DELETE' }); + const data = await response.json(); + if (!response.ok) { + alert(data.error || 'Failed to delete bucket'); + return; + } + alert('Bucket deleted'); + location.reload(); + } catch (error) { + alert('Failed to delete bucket: ' + error.message); + } +} + +async function loadS3TablesBucketPolicy(bucketArn) { + document.getElementById('s3tablesBucketPolicyText').value = ''; + if (!bucketArn) return; + try { + const response = await fetch(`/api/s3tables/bucket-policy?bucket=${encodeURIComponent(bucketArn)}`); + const data = await response.json(); + if (response.ok && data.policy) { + document.getElementById('s3tablesBucketPolicyText').value = data.policy; + } + } catch (error) { + console.error('Failed to load bucket policy', error); + } +} + +async function deleteS3TablesBucketPolicy() { + const bucketArn = document.getElementById('s3tablesBucketPolicyArn').value; + if (!bucketArn) return; + try { + const response = await fetch(`/api/s3tables/bucket-policy?bucket=${encodeURIComponent(bucketArn)}`, { method: 'DELETE' }); + const data = await response.json(); + if (!response.ok) { + alert(data.error || 'Failed to delete policy'); + return; + } + alert('Policy deleted'); + document.getElementById('s3tablesBucketPolicyText').value = ''; + } catch (error) { + alert('Failed to delete policy: ' + error.message); + } +} + +async function deleteS3TablesTable() { + const dataContainer = document.getElementById('s3tables-tables-content'); + const dataBucketArn = dataContainer.dataset.bucketArn || ''; + const dataNamespace = dataContainer.dataset.namespace || ''; + const tableName = document.getElementById('deleteS3TablesTableModal').dataset.tableName; + const versionToken = document.getElementById('deleteS3TablesTableVersion').value.trim(); + if (!tableName) return; + const query = new URLSearchParams({ + bucket: dataBucketArn, + namespace: dataNamespace, + name: tableName + }); + if (versionToken) { + query.set('version', versionToken); + } + try { + const response = await fetch(`/api/s3tables/tables?${query.toString()}`, { method: 'DELETE' }); + const data = await response.json(); + if (!response.ok) { + alert(data.error || 'Failed to delete table'); + return; + } + alert('Table deleted'); + location.reload(); + } catch (error) { + alert('Failed to delete table: ' + error.message); + } +} + +async function loadS3TablesTablePolicy(bucketArn, namespace, name) { + document.getElementById('s3tablesTablePolicyText').value = ''; + if (!bucketArn || !namespace || !name) return; + const query = new URLSearchParams({ bucket: bucketArn, namespace: namespace, name: name }); + try { + const response = await fetch(`/api/s3tables/table-policy?${query.toString()}`); + const data = await response.json(); + if (response.ok && data.policy) { + document.getElementById('s3tablesTablePolicyText').value = data.policy; + } + } catch (error) { + console.error('Failed to load table policy', error); + } +} + +async function deleteS3TablesTablePolicy() { + const dataContainer = document.getElementById('s3tables-tables-content'); + const dataBucketArn = dataContainer.dataset.bucketArn || ''; + const dataNamespace = dataContainer.dataset.namespace || ''; + const query = new URLSearchParams({ bucket: dataBucketArn, namespace: dataNamespace, name: document.getElementById('s3tablesTablePolicyName').value }); + try { + const response = await fetch(`/api/s3tables/table-policy?${query.toString()}`, { method: 'DELETE' }); + const data = await response.json(); + if (!response.ok) { + alert(data.error || 'Failed to delete policy'); + return; + } + alert('Policy deleted'); + document.getElementById('s3tablesTablePolicyText').value = ''; + } catch (error) { + alert('Failed to delete policy: ' + error.message); + } +} + +function parseTagsInput(input) { + if (!input) return {}; + const tags = {}; + const maxTags = 10; + const maxKeyLength = 128; + const maxValueLength = 256; + const parts = input.split(','); + for (const part of parts) { + const trimmedPart = part.trim(); + if (!trimmedPart) continue; + const idx = trimmedPart.indexOf('='); + if (idx <= 0) { + alert('Invalid tag format. Use key=value, and key cannot be empty.'); + return null; + } + const key = trimmedPart.slice(0, idx).trim(); + const value = trimmedPart.slice(idx + 1).trim(); + if (!key) { + alert('Invalid tag format. Use key=value, and key cannot be empty.'); + return null; + } + if (key.length > maxKeyLength) { + alert(`Tag key length must be <= ${maxKeyLength}`); + return null; + } + if (value.length > maxValueLength) { + alert(`Tag value length must be <= ${maxValueLength}`); + return null; + } + tags[key] = value; + if (Object.keys(tags).length > maxTags) { + alert(`Too many tags. Max ${maxTags} tags allowed.`); + return null; + } + } + return tags; +} + +async function openS3TablesTags(resourceArn) { + if (!resourceArn) return; + document.getElementById('s3tablesTagsResourceArn').value = resourceArn; + document.getElementById('s3tablesTagsInput').value = ''; + document.getElementById('s3tablesTagsDeleteInput').value = ''; + document.getElementById('s3tablesTagsList').textContent = 'Loading...'; + s3tablesTagsModal.show(); + try { + const response = await fetch(`/api/s3tables/tags?arn=${encodeURIComponent(resourceArn)}`); + const data = await response.json(); + if (response.ok) { + document.getElementById('s3tablesTagsList').textContent = JSON.stringify(data.tags || {}, null, 2); + } else { + document.getElementById('s3tablesTagsList').textContent = data.error || 'Failed to load tags'; + } + } catch (error) { + document.getElementById('s3tablesTagsList').textContent = error.message; + } +} + +async function updateS3TablesTags(resourceArn, tags) { + try { + const response = await fetch('/api/s3tables/tags', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ resource_arn: resourceArn, tags: tags }) + }); + const data = await response.json(); + if (!response.ok) { + alert(data.error || 'Failed to update tags'); + return; + } + alert('Tags updated'); + openS3TablesTags(resourceArn); + } catch (error) { + alert('Failed to update tags: ' + error.message); + } +} + +async function deleteS3TablesTags() { + const resourceArn = document.getElementById('s3tablesTagsResourceArn').value; + const keysInput = document.getElementById('s3tablesTagsDeleteInput').value.trim(); + if (!resourceArn) return; + const tagKeys = keysInput.split(',').map(k => k.trim()).filter(k => k); + if (tagKeys.length === 0) { + alert('Provide tag keys to remove'); + return; + } + try { + const response = await fetch('/api/s3tables/tags', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ resource_arn: resourceArn, tag_keys: tagKeys }) + }); + const data = await response.json(); + if (!response.ok) { + alert(data.error || 'Failed to remove tags'); + return; + } + alert('Tags removed'); + openS3TablesTags(resourceArn); + } catch (error) { + alert('Failed to remove tags: ' + error.message); + } +} diff --git a/weed/admin/view/app/file_browser.templ b/weed/admin/view/app/file_browser.templ index 38af7e7b2..58c999ebc 100644 --- a/weed/admin/view/app/file_browser.templ +++ b/weed/admin/view/app/file_browser.templ @@ -16,6 +16,8 @@ templ FileBrowser(data dash.FileBrowserData) {

if data.IsBucketPath && data.BucketName != "" { S3 Bucket: {data.BucketName} + } else if data.IsTableBucketPath && data.TableBucketName != "" { + Table Bucket: {data.TableBucketName} } else { File Browser } @@ -26,6 +28,10 @@ templ FileBrowser(data dash.FileBrowserData) { Back to Buckets + } else if data.IsTableBucketPath && data.TableBucketName != "" { + + Back to Table Buckets + }

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.IsBucketPath && data.BucketName != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "Back to Buckets ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "Back to Buckets ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.IsTableBucketPath && data.TableBucketName != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "Back to Table Buckets ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.CurrentPath == "/" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "Root Directory") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "Root Directory") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if data.CurrentPath == "/buckets" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "Object Store Buckets Directory Manage Buckets") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "Object Store Buckets Directory Manage Buckets") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.CurrentPath == "/table-buckets" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "Table Buckets Directory Manage Table Buckets") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" class=\"text-decoration-none text-primary\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(filepath.Base(data.CurrentPath)) + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(filepath.Base(data.CurrentPath)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 83, Col: 154} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 94, Col: 154} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + _, 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, 19, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -209,374 +233,374 @@ func FileBrowser(data dash.FileBrowserData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, ">200
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.HasNextPage { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "Next ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" class=\"btn btn-outline-primary\" title=\"Next page\">Next ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if data.ParentPath != data.CurrentPath { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " Up") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" class=\"btn btn-outline-secondary\" title=\"Go up one directory\"> Up") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(data.Entries) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
NameSizeTypeModifiedPermissionsActions
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, entry := range data.Entries { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "
NameSizeTypeModifiedPermissionsActions
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if entry.IsDirectory { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" class=\"text-decoration-none\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Name) + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 144, Col: 25} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 155, Col: 25} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + _, 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, 42, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - var templ_7745c5c3_Var15 = []any{fmt.Sprintf("fas %s text-muted me-2", getFileIcon(entry.Mime))} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...) + var templ_7745c5c3_Var16 = []any{fmt.Sprintf("fas %s text-muted me-2", getFileIcon(entry.Mime))} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\"> ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Name) + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 148, Col: 30} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 159, Col: 30} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if entry.IsDirectory { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(entry.Size)) + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(entry.Size)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 156, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 167, Col: 36} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + _, 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, 48, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if entry.IsDirectory { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "Directory") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "Directory") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - var templ_7745c5c3_Var19 string - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(getMimeDisplayName(entry.Mime)) + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(getMimeDisplayName(entry.Mime)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 164, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 175, Col: 44} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) + _, 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, 50, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if !entry.ModTime.IsZero() { - var templ_7745c5c3_Var20 string - templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(entry.ModTime.Format("2006-01-02 15:04")) + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(entry.ModTime.Format("2006-01-02 15:04")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 170, Col: 53} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 181, Col: 53} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Mode) + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Mode) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 176, Col: 146} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 187, Col: 146} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if !entry.IsDirectory { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\"> ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "
Empty Directory

This directory contains no files or subdirectories.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "
Empty Directory

This directory contains no files or subdirectories.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -584,123 +608,123 @@ func FileBrowser(data dash.FileBrowserData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, " entries per page
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, ">200 entries per page
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.HasNextPage { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "Next ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "\" class=\"btn btn-outline-primary\">Next ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "
Last updated: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "
Last updated: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) + var templ_7745c5c3_Var31 string + templ_7745c5c3_Var31, 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/file_browser.templ`, Line: 246, Col: 66} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 257, Col: 66} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "
Create New Folder
Folder names cannot contain / or \\ characters.
Create New Folder
Folder names cannot contain / or \\ characters.
Upload Files
Choose one or more files to upload to the current directory. You can select multiple files by holding Ctrl (Cmd on Mac) while clicking.
Upload Files
Choose one or more files to upload to the current directory. You can select multiple files by holding Ctrl (Cmd on Mac) while clicking.
0%
Preparing upload...
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "\">
0%
Preparing upload...
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/object_store_users.templ b/weed/admin/view/app/object_store_users.templ index ee90c1352..9a864bad5 100644 --- a/weed/admin/view/app/object_store_users.templ +++ b/weed/admin/view/app/object_store_users.templ @@ -220,6 +220,30 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { + + + + + + + + + + + + + + + + + + + + + + + + Hold Ctrl/Cmd to select multiple permissions @@ -304,6 +328,30 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { + + + + + + + + + + + + + + + + + + + + + + + +
@@ -457,6 +505,32 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { // Global variable to store available buckets var availableBuckets = []; var bucketPermissionCounter = 0; + const s3TablesPermissions = new Set([ + 'CreateTableBucket', + 'GetTableBucket', + 'ListTableBuckets', + 'DeleteTableBucket', + 'PutTableBucketPolicy', + 'GetTableBucketPolicy', + 'DeleteTableBucketPolicy', + 'CreateNamespace', + 'GetNamespace', + 'ListNamespaces', + 'DeleteNamespace', + 'CreateTable', + 'GetTable', + 'ListTables', + 'DeleteTable', + 'PutTablePolicy', + 'GetTablePolicy', + 'DeleteTablePolicy', + 'TagResource', + 'ListTagsForResource', + 'UntagResource' + ]); + function isS3TablesPermission(permission) { + return permission === 'S3TablesAdmin' || s3TablesPermissions.has(permission); + } // Load buckets async function loadBuckets() { @@ -464,10 +538,8 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { const response = await fetch('/api/s3/buckets'); if (response.ok) { const data = await response.json(); - availableBuckets = data.buckets || []; + availableBuckets = (data.buckets || []).map(bucket => ({ name: bucket.name, type: 's3' })); console.log('Loaded', availableBuckets.length, 'buckets'); - // Populate bucket selection dropdowns - populateBucketSelections(); } else { console.warn('Failed to load buckets'); availableBuckets = []; @@ -476,6 +548,20 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { console.error('Error loading buckets:', error); availableBuckets = []; } + try { + const response = await fetch('/api/s3tables/buckets'); + if (response.ok) { + const data = await response.json(); + const tableBuckets = (data.buckets || data.tableBuckets || []).map(bucket => ({ name: bucket.name, type: 's3tables' })); + availableBuckets = availableBuckets.concat(tableBuckets); + } else { + console.warn('Failed to load table buckets'); + } + } catch (error) { + console.warn('Error loading table buckets:', error); + } + // Populate bucket selection dropdowns + populateBucketSelections(); } // Load policies @@ -556,8 +642,8 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { select.innerHTML = ''; availableBuckets.forEach(bucket => { const option = document.createElement('option'); - option.value = bucket.name; - option.textContent = bucket.name; + option.value = bucket.type + ':' + bucket.name; + option.textContent = bucket.type === 's3tables' ? `Table: ${bucket.name}` : bucket.name; select.appendChild(option); }); } @@ -584,11 +670,25 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { const globalBucketPerms = []; actions.forEach(action => { - if (action.includes(':')) { + if (action.startsWith('s3tables:')) { + const actionValue = action.slice('s3tables:'.length); + if (actionValue === '*') { + globalBucketPerms.push('S3TablesAdmin'); + return; + } + const parts = actionValue.split(':'); + const perm = parts[0]; + const bucket = parts.length > 1 ? parts.slice(1).join(':').replace(/\/\*$/, '') : ''; + if (bucket) { + bucketActions.push({ permission: perm, bucketId: 's3tables:' + bucket }); + } else { + globalBucketPerms.push(perm); + } + } else if (action.includes(':')) { const parts = action.split(':'); const perm = parts[0]; const bucket = parts.slice(1).join(':').replace(/\/\*$/, ''); - bucketActions.push({ permission: perm, bucket: bucket }); + bucketActions.push({ permission: perm, bucketId: 's3:' + bucket }); } else { globalBucketPerms.push(action); } @@ -601,7 +701,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { } else if (bucketActions.length > 0) { // Get unique permissions and buckets const perms = [...new Set(bucketActions.map(ba => ba.permission))]; - const buckets = [...new Set(bucketActions.map(ba => ba.bucket))]; + const buckets = [...new Set(bucketActions.map(ba => ba.bucketId))]; result.permissions = perms; result.applyToAll = false; @@ -611,6 +711,16 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { return result; } + function parseBucketOptionValue(value) { + if (value.startsWith('s3tables:')) { + return { type: 's3tables', name: value.slice('s3tables:'.length) }; + } + if (value.startsWith('s3:')) { + return { type: 's3', name: value.slice('s3:'.length) }; + } + return { type: 's3', name: value }; + } + // Build bucket permission action strings using original permissions dropdown /** * Builds bucket permission strings based on selected permissions and bucket scope. @@ -627,10 +737,8 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { // Get selected permissions from the original multi-select const selectedPerms = Array.from(permSelect.selectedOptions).map(opt => opt.value); - // If Admin is selected, return just Admin (it overrides everything) - if (selectedPerms.includes('Admin')) { - return ['Admin']; - } + const hasAdmin = selectedPerms.includes('Admin'); + const hasS3TablesAdmin = selectedPerms.includes('S3TablesAdmin'); if (selectedPerms.length === 0) { return []; @@ -663,13 +771,30 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { if (applyToAll) { // Return global permissions (no bucket specification) - return selectedPerms; + const actions = []; + if (hasAdmin) { + actions.push('Admin'); + } + if (hasS3TablesAdmin) { + actions.push('s3tables:*'); + } + selectedPerms.forEach(perm => { + if (perm === 'Admin' || perm === 'S3TablesAdmin') { + return; + } + if (isS3TablesPermission(perm)) { + actions.push('s3tables:' + perm); + } else { + actions.push(perm); + } + }); + return actions; } else { // Get selected specific buckets const bucketSelect = document.getElementById(mode === 'edit' ? 'editSelectedBuckets' : 'selectedBuckets'); if (!bucketSelect) return null; - const selectedBuckets = Array.from(bucketSelect.selectedOptions).map(opt => opt.value); + const selectedBuckets = [...new Set(Array.from(bucketSelect.selectedOptions).map(opt => opt.value))]; // Return null to signal validation failure if no buckets selected if (selectedBuckets.length === 0) { @@ -678,13 +803,29 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { // Build bucket-scoped permissions const actions = []; + if (hasAdmin) { + actions.push('Admin'); + } + if (hasS3TablesAdmin) { + actions.push('s3tables:*'); + } selectedPerms.forEach(perm => { + if (perm === 'Admin' || perm === 'S3TablesAdmin') { + return; + } selectedBuckets.forEach(bucket => { - actions.push(perm + ':' + bucket); + const bucketInfo = parseBucketOptionValue(bucket); + if (isS3TablesPermission(perm)) { + if (bucketInfo.type === 's3tables') { + actions.push('s3tables:' + perm + ':' + bucketInfo.name); + } + } else if (bucketInfo.type === 's3') { + actions.push(perm + ':' + bucketInfo.name); + } }); }); - return actions; + return [...new Set(actions)]; } } @@ -834,6 +975,16 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { // Get permissions with bucket scope applied const allActions = buildBucketPermissions('create'); + if (allActions === null) { + showAlert('Please select at least one bucket when using specific bucket permissions', 'error'); + return; + } + + if (!allActions || allActions.length === 0) { + showAlert('At least one permission must be selected', 'error'); + return; + } + const userData = { username: formData.get('username'), email: formData.get('email'), @@ -887,15 +1038,15 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { // Get permissions with bucket scope applied const allActions = buildBucketPermissions('edit'); - // Validate that permissions are not empty - if (!allActions || allActions.length === 0) { - showAlert('At least one permission must be selected', 'error'); + // Check for null (validation failure from buildBucketPermissions) + if (allActions === null) { + showAlert('Please select at least one bucket when using specific bucket permissions', 'error'); return; } - // Check for null (validation failure from buildBucketPermissionsNew) - if (allActions === null) { - showAlert('Please select at least one bucket when using specific bucket permissions', 'error'); + // Validate that permissions are not empty + if (!allActions || allActions.length === 0) { + showAlert('At least one permission must be selected', 'error'); return; } @@ -1154,4 +1305,4 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { } // Helper functions for template - \ No newline at end of file + diff --git a/weed/admin/view/app/object_store_users_templ.go b/weed/admin/view/app/object_store_users_templ.go index 4791ea3c8..d43c4854b 100644 --- a/weed/admin/view/app/object_store_users_templ.go +++ b/weed/admin/view/app/object_store_users_templ.go @@ -193,7 +193,7 @@ func ObjectStoreUsers(data dash.ObjectStoreUsersData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
Create New User
Hold Ctrl/Cmd to select multiple permissions
Apply selected permissions to specific buckets or all buckets
Hold Ctrl/Cmd to select multiple buckets
Hold Ctrl/Cmd to select multiple policies
Edit User
Apply selected permissions to specific buckets or all buckets
Hold Ctrl/Cmd to select multiple buckets
User Details
Manage Access Keys
Access Keys for
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
Create New User
Hold Ctrl/Cmd to select multiple permissions
Apply selected permissions to specific buckets or all buckets
Hold Ctrl/Cmd to select multiple buckets
Hold Ctrl/Cmd to select multiple policies
Edit User
Apply selected permissions to specific buckets or all buckets
Hold Ctrl/Cmd to select multiple buckets
User Details
Manage Access Keys
Access Keys for
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/s3tables_buckets.templ b/weed/admin/view/app/s3tables_buckets.templ new file mode 100644 index 000000000..5642e7199 --- /dev/null +++ b/weed/admin/view/app/s3tables_buckets.templ @@ -0,0 +1,275 @@ +package app + +import ( + "fmt" + + "github.com/seaweedfs/seaweedfs/weed/admin/dash" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables" +) + +templ S3TablesBuckets(data dash.S3TablesBucketsData) { +
+

+ S3 Tables Buckets +

+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Total Buckets +
+
+ { fmt.Sprintf("%d", data.TotalBuckets) } +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Last Updated +
+
+ { data.LastUpdated.Format("15:04") } +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Table Buckets +
+
+
+
+ + + + + + + + + + + + for _, bucket := range data.Buckets { + + + + + + + + } + if len(data.Buckets) == 0 { + + + + } + +
NameOwnerARNCreatedActions
{ bucket.Name }{ bucket.OwnerAccountID }{ bucket.ARN }{ bucket.CreatedAt.Format("2006-01-02 15:04") } +
+ {{ bucketName, parseErr := s3tables.ParseBucketNameFromARN(bucket.ARN) }} + if parseErr == nil { + + + + } else { + + } + + + +
+
+ +
+
No table buckets found
+

Create your first S3 Tables bucket to get started.

+ +
+
+
+
+
+
+
+
+ + + + + +} diff --git a/weed/admin/view/app/s3tables_buckets_templ.go b/weed/admin/view/app/s3tables_buckets_templ.go new file mode 100644 index 000000000..c9270814b --- /dev/null +++ b/weed/admin/view/app/s3tables_buckets_templ.go @@ -0,0 +1,222 @@ +// 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, "

S3 Tables Buckets

Total Buckets
") + 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, "
Last Updated
") + 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, "
Table Buckets
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, bucket := range data.Buckets { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.Buckets) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
NameOwnerARNCreatedActions
") + 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, "") + 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, "") + 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, "") + 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, "
") + 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, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
No table buckets found

Create your first S3 Tables bucket to get started.

Create Table Bucket
The S3 identity that owns this table bucket. Non-admin users can only access table buckets they own.
Optional tags in key=value format.
Delete Table Bucket

Are you sure you want to delete the table bucket ?

Table Bucket Policy
Provide a policy JSON; use Delete Policy to remove the policy.
Resource Tags
Loading...
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/weed/admin/view/app/s3tables_namespaces.templ b/weed/admin/view/app/s3tables_namespaces.templ new file mode 100644 index 000000000..1b2e207b3 --- /dev/null +++ b/weed/admin/view/app/s3tables_namespaces.templ @@ -0,0 +1,242 @@ +package app + +import ( + "fmt" + "strings" + + "github.com/seaweedfs/seaweedfs/weed/admin/dash" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables" +) + +templ S3TablesNamespaces(data dash.S3TablesNamespacesData) { +
+

+ S3 Tables Namespaces +

+
+
+ +
+
+
+
+ + Back to Buckets + + Bucket ARN: { data.BucketARN } +
+
+
+
+
+
+
+
+
+ Total Namespaces +
+
+ { fmt.Sprintf("%d", data.TotalNamespaces) } +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Last Updated +
+
+ { data.LastUpdated.Format("15:04") } +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Namespaces +
+
+
+
+ + + + + + + + + + for _, namespace := range data.Namespaces { + + + + + + } + if len(data.Namespaces) == 0 { + + + + } + +
NamespaceCreatedActions
{ strings.Join(namespace.Namespace, ".") }{ namespace.CreatedAt.Format("2006-01-02 15:04") } +
+ {{ bucketName, parseErr := s3tables.ParseBucketNameFromARN(data.BucketARN) }} + {{ namespaceName := strings.Join(namespace.Namespace, ".") }} + if parseErr == nil { + + + + } else { + + } + +
+
+ +
+
No namespaces found
+

Create your first namespace to organize tables.

+ +
+
+
+
+
+
+
+
+ + + +} diff --git a/weed/admin/view/app/s3tables_namespaces_templ.go b/weed/admin/view/app/s3tables_namespaces_templ.go new file mode 100644 index 000000000..e18605cb9 --- /dev/null +++ b/weed/admin/view/app/s3tables_namespaces_templ.go @@ -0,0 +1,198 @@ +// 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" + "strings" + + "github.com/seaweedfs/seaweedfs/weed/admin/dash" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables" +) + +func S3TablesNamespaces(data dash.S3TablesNamespacesData) 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, "

S3 Tables Namespaces

Back to Buckets Bucket ARN: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.BucketARN) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_namespaces.templ`, Line: 28, Col: 60} + } + _, 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, "
Total Namespaces
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalNamespaces)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_namespaces.templ`, Line: 41, Col: 50} + } + _, 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, "
Last Updated
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, 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_namespaces.templ`, Line: 60, Col: 43} + } + _, 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, 5, "
Namespaces
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, namespace := range data.Namespaces { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.Namespaces) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
NamespaceCreatedActions
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(namespace.Namespace, ".")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_namespaces.templ`, Line: 92, Col: 55} + } + _, 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(namespace.CreatedAt.Format("2006-01-02 15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_namespaces.templ`, Line: 93, Col: 63} + } + _, 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + bucketName, parseErr := s3tables.ParseBucketNameFromARN(data.BucketARN) + namespaceName := strings.Join(namespace.Namespace, ".") + if parseErr == nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
No namespaces found

Create your first namespace to organize tables.

Create Namespace
Namespaces use a single level (no dots).
Delete Namespace

Are you sure you want to delete the namespace ?

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/weed/admin/view/app/s3tables_tables.templ b/weed/admin/view/app/s3tables_tables.templ new file mode 100644 index 000000000..d86ea545c --- /dev/null +++ b/weed/admin/view/app/s3tables_tables.templ @@ -0,0 +1,294 @@ +package app + +import ( + "fmt" + + "github.com/seaweedfs/seaweedfs/weed/admin/dash" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables" +) + +templ S3TablesTables(data dash.S3TablesTablesData) { +
+

+ S3 Tables +

+
+
+ +
+
+
+
+ {{ bucketName, parseErr := s3tables.ParseBucketNameFromARN(data.BucketARN) }} + if parseErr == nil { + + Back to Namespaces + + } else { + + } + Bucket ARN: { data.BucketARN } + Namespace: { data.Namespace } + if parseErr != nil { + Invalid bucket ARN + } +
+
+
+
+
+
+
+
+
+ Total Tables +
+
+ { fmt.Sprintf("%d", data.TotalTables) } +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Last Updated +
+
+ { data.LastUpdated.Format("15:04") } +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Tables +
+
+
+
+ + + + + + + + + + + + + for _, table := range data.Tables { + + {{ tableName := table.Name }} + + + + + + + + } + if len(data.Tables) == 0 { + + + + } + +
NameTable ARNCreatedModifiedMetadataActions
{ tableName }{ table.TableARN }{ table.CreatedAt.Format("2006-01-02 15:04") }{ table.ModifiedAt.Format("2006-01-02 15:04") } + if table.MetadataLocation != "" { + { table.MetadataLocation } + } else { + - + } + +
+ + + +
+
+ +
+
No tables found
+

Create your first table to start storing data.

+ +
+
+
+
+
+
+
+
+ + + + + +} diff --git a/weed/admin/view/app/s3tables_tables_templ.go b/weed/admin/view/app/s3tables_tables_templ.go new file mode 100644 index 000000000..3e2e4d8b3 --- /dev/null +++ b/weed/admin/view/app/s3tables_tables_templ.go @@ -0,0 +1,317 @@ +// 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 S3TablesTables(data dash.S3TablesTablesData) 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, "

S3 Tables

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + bucketName, parseErr := s3tables.ParseBucketNameFromARN(data.BucketARN) + if parseErr == nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "Back to Namespaces ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "Bucket ARN: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.BucketARN) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_tables.templ`, Line: 34, Col: 60} + } + _, 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, 6, " Namespace: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Namespace) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_tables.templ`, Line: 35, Col: 59} + } + _, 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, 7, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if parseErr != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "Invalid bucket ARN") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
Total Tables
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalTables)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_tables.templ`, Line: 51, Col: 46} + } + _, 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, 12, "
Last Updated
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, 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_tables.templ`, Line: 70, Col: 43} + } + _, 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, 13, "
Tables
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, table := range data.Tables { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + tableName := table.Name + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.Tables) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
NameTable ARNCreatedModifiedMetadataActions
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(tableName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_tables.templ`, Line: 106, Col: 26} + } + _, 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, 16, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(table.TableARN) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_tables.templ`, Line: 107, Col: 56} + } + _, 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, 17, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(table.CreatedAt.Format("2006-01-02 15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_tables.templ`, Line: 108, Col: 59} + } + _, 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, 18, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(table.ModifiedAt.Format("2006-01-02 15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_tables.templ`, Line: 109, Col: 60} + } + _, 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if table.MetadataLocation != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(table.MetadataLocation) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_tables.templ`, Line: 112, Col: 68} + } + _, 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, 21, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "-") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
No tables found

Create your first table to start storing data.

Create Table
Delete Table

Are you sure you want to delete the table ?

Table Policy
Resource Tags
Loading...
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/weed/admin/view/layout/layout.templ b/weed/admin/view/layout/layout.templ index c0173b2a2..68a262a29 100644 --- a/weed/admin/view/layout/layout.templ +++ b/weed/admin/view/layout/layout.templ @@ -163,6 +163,11 @@ templ Layout(c *gin.Context, content templ.Component) { Buckets +
MANAGEMENT
MANAGEMENT