diff --git a/test/s3tables/admin/admin_iceberg_pages_test.go b/test/s3tables/admin/admin_iceberg_pages_test.go new file mode 100644 index 000000000..4b6b2e410 --- /dev/null +++ b/test/s3tables/admin/admin_iceberg_pages_test.go @@ -0,0 +1,559 @@ +// Package admin contains integration tests for the admin UI's Iceberg +// catalog and data-preview pages. Tests start a real weed mini cluster +// (with the admin UI enabled), create a table bucket, namespace, and table +// via the S3 Tables manager, populate real Parquet data files, manifests, +// and snapshots via S3 and the filer gRPC API, and then assert on the HTML +// the admin server renders. +package admin + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/apache/iceberg-go" + "github.com/apache/iceberg-go/table" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/parquet-go/parquet-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/seaweedfs/seaweedfs/test/testutil" + "github.com/seaweedfs/seaweedfs/weed/command" + "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" +) + +// --------------------------------------------------------------------------- +// Cluster lifecycle (mirrors test/s3tables/maintenance) +// --------------------------------------------------------------------------- + +type testCluster struct { + dataDir string + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + filerGrpcPort int + s3Port int + adminPort int + s3Endpoint string + adminEndpoint string + isRunning bool +} + +var shared *testCluster + +func TestMain(m *testing.M) { + flag.Parse() + if testing.Short() { + os.Exit(m.Run()) + } + + testDir, err := os.MkdirTemp("", "seaweed-admin-iceberg-*") + if err != nil { + fmt.Fprintf(os.Stderr, "SKIP: failed to create temp dir: %v\n", err) + os.Exit(0) + } + + cluster, err := startCluster(testDir) + if err != nil { + fmt.Fprintf(os.Stderr, "SKIP: failed to start cluster: %v\n", err) + os.RemoveAll(testDir) + os.Exit(0) + } + shared = cluster + + code := m.Run() + shared.stop() + os.RemoveAll(testDir) + os.Exit(code) +} + +func startCluster(testDir string) (*testCluster, error) { + ports, err := testutil.AllocatePorts(10) + if err != nil { + return nil, err + } + masterPort, masterGrpc := ports[0], ports[1] + volumePort, volumeGrpc := ports[2], ports[3] + filerPort, filerGrpc := ports[4], ports[5] + s3Port, s3Grpc := ports[6], ports[7] + adminPort, adminGrpc := ports[8], ports[9] + + // Empty security.toml disables JWT auth. + if err := os.WriteFile(filepath.Join(testDir, "security.toml"), []byte("# test\n"), 0644); err != nil { + return nil, err + } + if os.Getenv("AWS_ACCESS_KEY_ID") == "" { + os.Setenv("AWS_ACCESS_KEY_ID", "admin") + } + if os.Getenv("AWS_SECRET_ACCESS_KEY") == "" { + os.Setenv("AWS_SECRET_ACCESS_KEY", "admin") + } + + ctx, cancel := context.WithCancel(context.Background()) + c := &testCluster{ + dataDir: testDir, + ctx: ctx, + cancel: cancel, + filerGrpcPort: filerGrpc, + s3Port: s3Port, + adminPort: adminPort, + s3Endpoint: fmt.Sprintf("http://127.0.0.1:%d", s3Port), + adminEndpoint: fmt.Sprintf("http://127.0.0.1:%d", adminPort), + } + + // Set on the package global before the goroutine starts so the mini + // command picks it up; cleared in stop(). The global is how the mini + // harness receives its cancellation context in tests. + command.MiniClusterCtx = ctx //nolint:fatcontext // test harness handoff, not a per-request context + + c.wg.Add(1) + go func() { + defer c.wg.Done() + oldDir, _ := os.Getwd() + oldArgs := os.Args + defer func() { os.Chdir(oldDir); os.Args = oldArgs }() + os.Chdir(testDir) + + args := []string{ + "-dir=" + testDir, + "-master.dir=" + testDir, + "-master.port=" + strconv.Itoa(masterPort), + "-master.port.grpc=" + strconv.Itoa(masterGrpc), + "-volume.port=" + strconv.Itoa(volumePort), + "-volume.port.grpc=" + strconv.Itoa(volumeGrpc), + "-volume.port.public=" + strconv.Itoa(volumePort), + "-volume.publicUrl=127.0.0.1:" + strconv.Itoa(volumePort), + "-filer.port=" + strconv.Itoa(filerPort), + "-filer.port.grpc=" + strconv.Itoa(filerGrpc), + "-s3.port=" + strconv.Itoa(s3Port), + "-s3.port.grpc=" + strconv.Itoa(s3Grpc), + "-admin.port=" + strconv.Itoa(adminPort), + "-admin.port.grpc=" + strconv.Itoa(adminGrpc), + "-webdav.port=0", + "-master.volumeSizeLimitMB=32", + "-ip=127.0.0.1", + "-master.peers=none", + "-s3.iam.readOnly=false", + } + os.Args = append([]string{"weed"}, args...) + glog.MaxSize = 1024 * 1024 + for _, cmd := range command.Commands { + if cmd.Name() == "mini" && cmd.Run != nil { + cmd.Flag.Parse(os.Args[1:]) + cmd.Run(cmd, cmd.Flag.Args()) + return + } + } + }() + + if err := waitReady(c.s3Endpoint, 30*time.Second); err != nil { + cancel() + return nil, err + } + if err := waitReady(c.adminEndpoint, 30*time.Second); err != nil { + cancel() + return nil, err + } + c.isRunning = true + return c, nil +} + +func (c *testCluster) stop() { + command.MiniClusterCtx = nil //nolint:fatcontext // clearing the test harness handoff + if c.cancel != nil { + c.cancel() + } + if c.isRunning { + time.Sleep(500 * time.Millisecond) + } + done := make(chan struct{}) + go func() { c.wg.Wait(); close(done) }() + select { + case <-done: + case <-time.After(2 * time.Second): + } +} + +func (c *testCluster) filerConn(t *testing.T) filer_pb.SeaweedFilerClient { + t.Helper() + addr := fmt.Sprintf("127.0.0.1:%d", c.filerGrpcPort) + conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + t.Cleanup(func() { conn.Close() }) + return filer_pb.NewSeaweedFilerClient(conn) +} + +func waitReady(endpoint string, timeout time.Duration) error { + client := &http.Client{Timeout: 1 * time.Second} + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + resp, err := client.Get(endpoint) + if err == nil { + resp.Body.Close() + time.Sleep(500 * time.Millisecond) + return nil + } + time.Sleep(200 * time.Millisecond) + } + return fmt.Errorf("timeout waiting for %s", endpoint) +} + +func randomSuffix() string { + b := make([]byte, 4) + rand.Read(b) + return fmt.Sprintf("%x", b) +} + +// --------------------------------------------------------------------------- +// Catalog and table population +// --------------------------------------------------------------------------- + +type previewRow struct { + ID int64 `parquet:"id"` + Name string `parquet:"name"` +} + +func buildParquet(t *testing.T, rows []previewRow) []byte { + t.Helper() + var buf bytes.Buffer + writer := parquet.NewGenericWriter[previewRow](&buf) + _, err := writer.Write(rows) + require.NoError(t, err) + require.NoError(t, writer.Close()) + return buf.Bytes() +} + +func newS3Client(t *testing.T, endpoint string) *s3.Client { + t.Helper() + cfg, err := config.LoadDefaultConfig(context.Background(), + config.WithRegion("us-east-1"), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("admin", "admin", "")), + ) + require.NoError(t, err) + return s3.NewFromConfig(cfg, func(o *s3.Options) { + o.BaseEndpoint = aws.String(endpoint) + o.UsePathStyle = true + }) +} + +type catalogFixture struct { + bucket string + namespace string + table string + snap1ID int64 + snap2ID int64 + file1Path string // manifest-listed location of the first data file (relative) + file2Path string // manifest-listed location of the second data file (s3:// URI) +} + +// setupCatalog creates a table bucket, namespace, and two tables via the S3 +// Tables manager: "events" with two snapshots backed by real Parquet files, +// and "empty" with no Iceberg metadata. +func setupCatalog(t *testing.T, client filer_pb.SeaweedFilerClient) catalogFixture { + t.Helper() + ctx := context.Background() + + fx := catalogFixture{ + bucket: "admin-ui-" + randomSuffix(), + namespace: "analytics", + table: "events", + snap1ID: 3001, + snap2ID: 3002, + } + fx.file1Path = "data/p1.parquet" + fx.file2Path = fmt.Sprintf("s3://%s/%s/%s/data/p2.parquet", fx.bucket, fx.namespace, fx.table) + + mgr := s3tables.NewManager() + mgr.SetAccountID(s3_constants.AccountAdminId) + mc := s3tables.NewManagerClient(client) + exec := func(op string, req, resp interface{}) { + t.Helper() + require.NoError(t, mgr.Execute(ctx, mc, op, req, resp, s3_constants.AccountAdminId), "s3tables %s", op) + } + + var bucketResp s3tables.CreateTableBucketResponse + exec("CreateTableBucket", &s3tables.CreateTableBucketRequest{Name: fx.bucket}, &bucketResp) + exec("CreateNamespace", &s3tables.CreateNamespaceRequest{TableBucketARN: bucketResp.ARN, Namespace: []string{fx.namespace}}, &s3tables.CreateNamespaceResponse{}) + exec("CreateTable", &s3tables.CreateTableRequest{TableBucketARN: bucketResp.ARN, Namespace: []string{fx.namespace}, Name: fx.table, Format: "ICEBERG"}, &s3tables.CreateTableResponse{}) + exec("CreateTable", &s3tables.CreateTableRequest{TableBucketARN: bucketResp.ARN, Namespace: []string{fx.namespace}, Name: "empty", Format: "ICEBERG"}, &s3tables.CreateTableResponse{}) + + // Upload real Parquet data files via S3 so they are chunk-backed entries. + s3Client := newS3Client(t, shared.s3Endpoint) + tableKey := path.Join(fx.namespace, fx.table) + rows1 := []previewRow{{1, "p1-row-1"}, {2, "p1-row-2"}, {3, "p1-row-3"}, {4, "p1-row-4"}, {5, "p1-row-5"}} + rows2 := []previewRow{{6, "p2-row-6"}, {7, "p2-row-7"}, {8, "p2-row-8"}} + parquet1 := buildParquet(t, rows1) + parquet2 := buildParquet(t, rows2) + putObject(t, s3Client, fx.bucket, path.Join(tableKey, "data/p1.parquet"), parquet1) + putObject(t, s3Client, fx.bucket, path.Join(tableKey, "data/p2.parquet"), parquet2) + + // Build table metadata with two snapshots. Snapshot 2's manifest list is + // cumulative (both manifests) as engines write it. + location := fmt.Sprintf("s3://%s/%s/%s", fx.bucket, fx.namespace, fx.table) + schema := iceberg.NewSchema(0, + iceberg.NestedField{ID: 1, Type: iceberg.PrimitiveTypes.Int64, Name: "id", Required: true}, + iceberg.NestedField{ID: 2, Type: iceberg.PrimitiveTypes.String, Name: "name", Required: false}, + ) + meta, err := table.NewMetadata(schema, iceberg.UnpartitionedSpec, table.UnsortedSortOrder, location, nil) + require.NoError(t, err) + + baseMs := time.Now().UnixMilli() + 100 + snap1 := table.Snapshot{SnapshotID: fx.snap1ID, SequenceNumber: 1, TimestampMs: baseMs, ManifestList: "metadata/snap-3001.avro"} + snap2 := table.Snapshot{SnapshotID: fx.snap2ID, ParentSnapshotID: &fx.snap1ID, SequenceNumber: 2, TimestampMs: baseMs + 1, ManifestList: "metadata/snap-3002.avro"} + + builder, err := table.MetadataBuilderFromBase(meta, location) + require.NoError(t, err) + require.NoError(t, builder.AddSnapshot(&snap1)) + require.NoError(t, builder.AddSnapshot(&snap2)) + require.NoError(t, builder.SetSnapshotRef(table.MainBranch, fx.snap2ID, table.BranchRef)) + meta, err = builder.Build() + require.NoError(t, err) + + // Write manifests and manifest lists into the table's metadata directory. + metaDir := path.Join(s3tables.TablesPath, fx.bucket, fx.namespace, fx.table, "metadata") + spec := meta.PartitionSpec() + version := meta.Version() + + mf1 := writeManifest(t, ctx, client, metaDir, "manifest-3001.avro", version, spec, schema, fx.snap1ID, fx.file1Path, int64(len(rows1)), int64(len(parquet1))) + mf2 := writeManifest(t, ctx, client, metaDir, "manifest-3002.avro", version, spec, schema, fx.snap2ID, fx.file2Path, int64(len(rows2)), int64(len(parquet2))) + + list1 := writeManifestList(t, ctx, client, metaDir, "snap-3001.avro", version, fx.snap1ID, nil, 1, []iceberg.ManifestFile{mf1}) + // Re-read snapshot 1's list so mf1 carries its assigned sequence number, + // which the cumulative snapshot 2 list requires. + assigned, err := iceberg.ReadManifestList(bytes.NewReader(list1)) + require.NoError(t, err) + require.Len(t, assigned, 1) + writeManifestList(t, ctx, client, metaDir, "snap-3002.avro", version, fx.snap2ID, &fx.snap1ID, 2, []iceberg.ManifestFile{assigned[0], mf2}) + + // Point the table's catalog xattr at the new metadata. + fullJSON, err := json.Marshal(meta) + require.NoError(t, err) + writeFile(t, ctx, client, metaDir, "v2.metadata.json", fullJSON) + updateTableMetadataXattr(t, ctx, client, fx, fullJSON) + + // Snapshot timestamps sit slightly in the future; let them pass. + time.Sleep(200 * time.Millisecond) + return fx +} + +func putObject(t *testing.T, client *s3.Client, bucket, key string, body []byte) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + _, err := client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(key), + Body: bytes.NewReader(body), + }) + require.NoError(t, err) +} + +func writeManifest(t *testing.T, ctx context.Context, client filer_pb.SeaweedFilerClient, metaDir, name string, version int, spec iceberg.PartitionSpec, schema *iceberg.Schema, snapID int64, dataFilePath string, recordCount, fileSize int64) iceberg.ManifestFile { + t.Helper() + dfBuilder, err := iceberg.NewDataFileBuilder( + spec, iceberg.EntryContentData, + dataFilePath, iceberg.ParquetFile, + map[int]any{}, nil, nil, recordCount, fileSize, + ) + require.NoError(t, err) + entry := iceberg.NewManifestEntry(iceberg.EntryStatusADDED, &snapID, nil, nil, dfBuilder.Build()) + + var buf bytes.Buffer + mf, err := iceberg.WriteManifest(path.Join("metadata", name), &buf, version, spec, schema, snapID, []iceberg.ManifestEntry{entry}) + require.NoError(t, err) + writeFile(t, ctx, client, metaDir, name, buf.Bytes()) + return mf +} + +func writeManifestList(t *testing.T, ctx context.Context, client filer_pb.SeaweedFilerClient, metaDir, name string, version int, snapID int64, parent *int64, seqNum int64, files []iceberg.ManifestFile) []byte { + t.Helper() + var buf bytes.Buffer + require.NoError(t, iceberg.WriteManifestList(version, &buf, snapID, parent, &seqNum, 0, files)) + writeFile(t, ctx, client, metaDir, name, buf.Bytes()) + return buf.Bytes() +} + +func writeFile(t *testing.T, ctx context.Context, client filer_pb.SeaweedFilerClient, dir, name string, content []byte) { + t.Helper() + resp, err := client.CreateEntry(ctx, &filer_pb.CreateEntryRequest{ + Directory: dir, + Entry: &filer_pb.Entry{ + Name: name, + Attributes: &filer_pb.FuseAttributes{ + Mtime: time.Now().Unix(), Crtime: time.Now().Unix(), + FileMode: uint32(0644), FileSize: uint64(len(content)), + }, + Content: content, + }, + }) + require.NoError(t, err, "writeFile(%s, %s)", dir, name) + require.Empty(t, resp.Error, "writeFile(%s, %s)", dir, name) +} + +func updateTableMetadataXattr(t *testing.T, ctx context.Context, client filer_pb.SeaweedFilerClient, fx catalogFixture, fullJSON []byte) { + t.Helper() + nsDir := path.Join(s3tables.TablesPath, fx.bucket, fx.namespace) + resp, err := filer_pb.LookupEntry(ctx, client, &filer_pb.LookupDirectoryEntryRequest{Directory: nsDir, Name: fx.table}) + require.NoError(t, err) + require.NotNil(t, resp.Entry) + + var internalMeta map[string]json.RawMessage + require.NoError(t, json.Unmarshal(resp.Entry.Extended[s3tables.ExtendedKeyMetadata], &internalMeta)) + metaObj := map[string]json.RawMessage{} + if raw, ok := internalMeta["metadata"]; ok { + require.NoError(t, json.Unmarshal(raw, &metaObj)) + } + metaObj["fullMetadata"] = fullJSON + metaJSON, err := json.Marshal(metaObj) + require.NoError(t, err) + internalMeta["metadata"] = metaJSON + internalMeta["metadataVersion"] = json.RawMessage("2") + internalMeta["metadataLocation"] = json.RawMessage(`"metadata/v2.metadata.json"`) + xattr, err := json.Marshal(internalMeta) + require.NoError(t, err) + + resp.Entry.Extended[s3tables.ExtendedKeyMetadata] = xattr + resp.Entry.Extended[s3tables.ExtendedKeyMetadataVersion] = []byte("2") + _, err = client.UpdateEntry(ctx, &filer_pb.UpdateEntryRequest{Directory: nsDir, Entry: resp.Entry}) + require.NoError(t, err) +} + +// --------------------------------------------------------------------------- +// Page fetching +// --------------------------------------------------------------------------- + +func fetchPage(t *testing.T, pagePath string) string { + t.Helper() + resp, err := (&http.Client{Timeout: 30 * time.Second}).Get(shared.adminEndpoint + pagePath) + require.NoError(t, err, "GET %s", pagePath) + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode, "GET %s: %s", pagePath, string(body)) + return string(body) +} + +// fetchPageUntil re-fetches a page until the predicate passes, tolerating the +// admin server's filer-discovery delay right after startup. +func fetchPageUntil(t *testing.T, pagePath string, predicate func(string) bool) string { + t.Helper() + deadline := time.Now().Add(15 * time.Second) + var body string + for time.Now().Before(deadline) { + body = fetchPage(t, pagePath) + if predicate(body) { + return body + } + time.Sleep(500 * time.Millisecond) + } + return body +} + +func tablePagePath(fx catalogFixture, tableName, suffix string) string { + return fmt.Sprintf("/object-store/s3tables/buckets/%s/namespaces/%s/tables/%s%s", + url.PathEscape(fx.bucket), url.PathEscape(fx.namespace), url.PathEscape(tableName), suffix) +} + +// --------------------------------------------------------------------------- +// Integration tests +// --------------------------------------------------------------------------- + +func TestAdminIcebergPages(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + client := shared.filerConn(t) + fx := setupCatalog(t, client) + + t.Run("CatalogBrowsing", func(t *testing.T) { + buckets := fetchPageUntil(t, "/object-store/s3tables/buckets", func(body string) bool { + return strings.Contains(body, fx.bucket) + }) + assert.Contains(t, buckets, fx.bucket) + + namespaces := fetchPage(t, fmt.Sprintf("/object-store/s3tables/buckets/%s/namespaces", url.PathEscape(fx.bucket))) + assert.Contains(t, namespaces, fx.namespace) + + tables := fetchPage(t, fmt.Sprintf("/object-store/s3tables/buckets/%s/namespaces/%s/tables", url.PathEscape(fx.bucket), url.PathEscape(fx.namespace))) + assert.Contains(t, tables, fx.table) + assert.Contains(t, tables, "empty") + }) + + t.Run("TableDetails", func(t *testing.T) { + body := fetchPage(t, tablePagePath(fx, fx.table, "")) + assert.Contains(t, body, "Browse Data") + assert.Contains(t, body, "/data") + assert.Contains(t, body, ">id<") + assert.Contains(t, body, ">name<") + assert.Contains(t, body, strconv.FormatInt(fx.snap1ID, 10)) + assert.Contains(t, body, strconv.FormatInt(fx.snap2ID, 10)) + }) + + t.Run("DataPreviewCurrentSnapshot", func(t *testing.T) { + body := fetchPage(t, tablePagePath(fx, fx.table, "/data")) + assert.Contains(t, body, "Showing 8 row(s) from 2 data file(s).") + assert.Contains(t, body, "p1-row-1") + assert.Contains(t, body, "p2-row-8") + assert.Contains(t, body, fx.file1Path) + assert.Contains(t, body, fx.file2Path) + assert.Contains(t, body, "current") + }) + + t.Run("DataPreviewOldSnapshot", func(t *testing.T) { + body := fetchPage(t, tablePagePath(fx, fx.table, "/data?snapshot="+strconv.FormatInt(fx.snap1ID, 10))) + assert.Contains(t, body, "Showing 5 row(s) from 1 data file(s).") + assert.Contains(t, body, "p1-row-5") + assert.NotContains(t, body, "p2-row-6") + }) + + t.Run("DataPreviewSingleFile", func(t *testing.T) { + body := fetchPage(t, tablePagePath(fx, fx.table, "/data?file="+url.QueryEscape(fx.file2Path))) + assert.Contains(t, body, "Showing 3 row(s) from 1 data file(s).") + assert.Contains(t, body, "p2-row-6") + assert.NotContains(t, body, "p1-row-1") + }) + + t.Run("DataPreviewRowLimit", func(t *testing.T) { + body := fetchPage(t, tablePagePath(fx, fx.table, "/data?limit=3")) + assert.Contains(t, body, "Showing 3 row(s) from 1 data file(s).") + }) + + t.Run("DataPreviewUnknownFile", func(t *testing.T) { + body := fetchPage(t, tablePagePath(fx, fx.table, "/data?file="+url.QueryEscape("s3://elsewhere/x.parquet"))) + assert.Contains(t, body, "Requested data file is not part of this snapshot.") + }) + + t.Run("DataPreviewUnknownSnapshot", func(t *testing.T) { + body := fetchPage(t, tablePagePath(fx, fx.table, "/data?snapshot=42")) + assert.Contains(t, body, "Snapshot 42 not found.") + }) + + t.Run("DataPreviewTableWithoutMetadata", func(t *testing.T) { + body := fetchPage(t, tablePagePath(fx, "empty", "/data")) + assert.Contains(t, body, "Table has no Iceberg metadata.") + }) +} diff --git a/weed/admin/dash/client_management.go b/weed/admin/dash/client_management.go index 6867782f4..46948a628 100644 --- a/weed/admin/dash/client_management.go +++ b/weed/admin/dash/client_management.go @@ -12,6 +12,8 @@ import ( "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/pb/master_pb" "github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb" + "github.com/seaweedfs/seaweedfs/weed/security" + "github.com/seaweedfs/seaweedfs/weed/util" "github.com/seaweedfs/seaweedfs/weed/wdclient" "google.golang.org/grpc" ) @@ -55,6 +57,20 @@ func (s *AdminServer) GetGrpcDialOption() grpc.DialOption { return s.grpcDialOption } +// VolumeServerReadJwt mints a per-fileId Bearer token for reads against a +// volume server when jwt.signing.read.key is configured. The volume servers +// are unaware of jwt.filer_signing.read.key — that one only gates the filer +// HTTP surface, which this code path doesn't touch. +func VolumeServerReadJwt(fileId string) string { + v := util.GetViper() + signingKey := security.SigningKey(v.GetString("jwt.signing.read.key")) + if len(signingKey) == 0 { + return "" + } + expiresAfterSec := v.GetInt("jwt.signing.read.expires_after_seconds") + return string(security.GenJwtForVolumeServer(signingKey, expiresAfterSec, fileId)) +} + // GetFilerAddress returns a filer address, discovering from masters if needed func (s *AdminServer) GetFilerAddress() string { // Discover filers from masters diff --git a/weed/admin/dash/iceberg_data_preview.go b/weed/admin/dash/iceberg_data_preview.go new file mode 100644 index 000000000..872140097 --- /dev/null +++ b/weed/admin/dash/iceberg_data_preview.go @@ -0,0 +1,469 @@ +package dash + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "path" + "slices" + "strings" + "time" + "unicode/utf8" + + "github.com/apache/iceberg-go" + "github.com/parquet-go/parquet-go" + "github.com/seaweedfs/seaweedfs/weed/filer" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables" +) + +const ( + icebergPreviewDefaultRows = 50 + icebergPreviewMaxRows = 200 + icebergPreviewMaxFiles = 5 + icebergPreviewMaxManifests = 100 + icebergPreviewMaxListed = 500 + icebergPreviewMaxCellChars = 200 + icebergPreviewMaxMetaBytes = 64 << 20 +) + +type IcebergDataFileInfo struct { + Path string `json:"path"` + Format string `json:"format"` + RecordCount int64 `json:"record_count"` + SizeBytes int64 `json:"size_bytes"` +} + +// IcebergDataPreviewData backs the table data-preview page. PreviewError and +// PreviewNotes report per-snapshot problems without failing the whole page. +type IcebergDataPreviewData struct { + Username string `json:"username"` + CatalogName string `json:"catalog_name"` + NamespaceName string `json:"namespace_name"` + TableName string `json:"table_name"` + BucketARN string `json:"bucket_arn"` + SnapshotID int64 `json:"snapshot_id"` + SnapshotTime time.Time `json:"snapshot_time"` + CurrentSnapshotID int64 `json:"current_snapshot_id"` + Snapshots []IcebergSnapshotInfo `json:"snapshots"` + Columns []string `json:"columns"` + Rows [][]string `json:"rows"` + RowLimit int `json:"row_limit"` + ScannedFiles int `json:"scanned_files"` + SelectedFile string `json:"selected_file"` + DataFiles []IcebergDataFileInfo `json:"data_files"` + TotalDataFiles int `json:"total_data_files"` + TotalRecords int64 `json:"total_records"` + HasDeletes bool `json:"has_deletes"` + PreviewNotes []string `json:"preview_notes,omitempty"` + PreviewError string `json:"preview_error,omitempty"` + LastUpdated time.Time `json:"last_updated"` +} + +// GetIcebergTableDataPreview walks the selected snapshot's manifests and reads +// sample rows from its Parquet data files. snapshotID 0 means the current +// snapshot; selectedFile restricts the preview to one manifest-listed file. +func (s *AdminServer) GetIcebergTableDataPreview(ctx context.Context, catalogName, bucketArn, namespace, tableName string, snapshotID int64, selectedFile string, rowLimit int) (IcebergDataPreviewData, error) { + if rowLimit < 1 { + rowLimit = icebergPreviewDefaultRows + } + if rowLimit > icebergPreviewMaxRows { + rowLimit = icebergPreviewMaxRows + } + + data := IcebergDataPreviewData{ + CatalogName: catalogName, + NamespaceName: namespace, + TableName: tableName, + BucketARN: bucketArn, + RowLimit: rowLimit, + SelectedFile: selectedFile, + LastUpdated: time.Now(), + } + + namespaceParts, err := parseNamespaceInput(namespace) + if err != nil { + return data, err + } + var resp s3tables.GetTableResponse + req := &s3tables.GetTableRequest{TableBucketARN: bucketArn, Namespace: namespaceParts, Name: tableName} + if err := s.executeS3TablesOperation(ctx, "GetTable", req, &resp); err != nil { + return data, err + } + if resp.Metadata == nil || len(resp.Metadata.FullMetadata) == 0 { + data.PreviewError = "Table has no Iceberg metadata." + return data, nil + } + var full icebergFullMetadata + if err := json.Unmarshal(resp.Metadata.FullMetadata, &full); err != nil { + data.PreviewError = fmt.Sprintf("Failed to parse Iceberg metadata: %v", err) + return data, nil + } + data.Snapshots = snapshotsFromFullMetadata(full.Snapshots) + data.CurrentSnapshotID = full.CurrentSnapshotID + + snap := selectPreviewSnapshot(full, snapshotID) + if snap == nil { + if snapshotID != 0 { + data.PreviewError = fmt.Sprintf("Snapshot %d not found.", snapshotID) + } else { + data.PreviewError = "Table has no snapshots yet." + } + return data, nil + } + data.SnapshotID = snap.SnapshotID + if snap.TimestampMs > 0 { + data.SnapshotTime = time.Unix(0, snap.TimestampMs*int64(time.Millisecond)) + } + if snap.ManifestList == "" { + data.PreviewError = "Snapshot has no manifest list." + return data, nil + } + + dataFiles, hasDeletes, notes, err := s.listIcebergDataFiles(ctx, catalogName, namespace, tableName, snap.ManifestList) + if err != nil { + data.PreviewError = fmt.Sprintf("Failed to read snapshot manifests: %v", err) + return data, nil + } + data.HasDeletes = hasDeletes + data.PreviewNotes = notes + data.TotalDataFiles = len(dataFiles) + for _, f := range dataFiles { + data.TotalRecords += f.RecordCount + } + data.DataFiles = dataFiles + if len(dataFiles) > icebergPreviewMaxListed { + data.DataFiles = dataFiles[:icebergPreviewMaxListed] + data.PreviewNotes = append(data.PreviewNotes, fmt.Sprintf("Listing first %d of %d data files.", icebergPreviewMaxListed, len(dataFiles))) + } + + toScan := dataFiles + if selectedFile != "" { + toScan = nil + for _, f := range dataFiles { + if f.Path == selectedFile { + toScan = []IcebergDataFileInfo{f} + break + } + } + if toScan == nil { + data.PreviewError = "Requested data file is not part of this snapshot." + return data, nil + } + } + if len(toScan) == 0 { + data.PreviewNotes = append(data.PreviewNotes, "Snapshot contains no data files.") + return data, nil + } + + for _, f := range toScan { + if len(data.Rows) >= rowLimit || data.ScannedFiles >= icebergPreviewMaxFiles { + break + } + if !strings.EqualFold(f.Format, string(iceberg.ParquetFile)) { + data.PreviewNotes = append(data.PreviewNotes, fmt.Sprintf("Skipped %s: %s preview is not supported.", path.Base(f.Path), f.Format)) + continue + } + filerPath, err := icebergLocationToFilerPath(f.Path, catalogName, namespace, tableName) + if err != nil { + data.PreviewNotes = append(data.PreviewNotes, fmt.Sprintf("Skipped %s: %v", path.Base(f.Path), err)) + continue + } + cols, rows, err := s.readParquetRows(ctx, filerPath, rowLimit-len(data.Rows)) + if err != nil { + data.PreviewNotes = append(data.PreviewNotes, fmt.Sprintf("Failed to read %s: %v", path.Base(f.Path), err)) + data.ScannedFiles++ + continue + } + if data.Columns == nil { + data.Columns = cols + } else if !slices.Equal(data.Columns, cols) { + data.PreviewNotes = append(data.PreviewNotes, fmt.Sprintf("Skipped %s: column layout differs from the first file.", path.Base(f.Path))) + continue + } + data.Rows = append(data.Rows, rows...) + data.ScannedFiles++ + } + if hasDeletes { + data.PreviewNotes = append(data.PreviewNotes, "Snapshot has row-level delete files; the preview shows raw data-file rows without applying deletes.") + } + return data, nil +} + +func selectPreviewSnapshot(full icebergFullMetadata, snapshotID int64) *icebergSnapshot { + if snapshotID != 0 { + for i := range full.Snapshots { + if full.Snapshots[i].SnapshotID == snapshotID { + return &full.Snapshots[i] + } + } + return nil + } + return selectSnapshotForMetrics(full) +} + +func (s *AdminServer) listIcebergDataFiles(ctx context.Context, bucketName, namespaceDir, tableName, manifestListLocation string) (files []IcebergDataFileInfo, hasDeletes bool, notes []string, err error) { + listPath, err := icebergLocationToFilerPath(manifestListLocation, bucketName, namespaceDir, tableName) + if err != nil { + return nil, false, nil, err + } + listBytes, err := s.readFilerFileContent(ctx, listPath) + if err != nil { + return nil, false, nil, fmt.Errorf("read manifest list %s: %w", manifestListLocation, err) + } + manifests, err := iceberg.ReadManifestList(bytes.NewReader(listBytes)) + if err != nil { + return nil, false, nil, fmt.Errorf("parse manifest list: %w", err) + } + + scanned := 0 + for _, mf := range manifests { + if mf.ManifestContent() != iceberg.ManifestContentData { + hasDeletes = true + continue + } + if scanned >= icebergPreviewMaxManifests { + notes = append(notes, fmt.Sprintf("Scanned first %d of %d manifests.", icebergPreviewMaxManifests, len(manifests))) + break + } + scanned++ + mfPath, err := icebergLocationToFilerPath(mf.FilePath(), bucketName, namespaceDir, tableName) + if err != nil { + notes = append(notes, fmt.Sprintf("Skipped manifest %s: %v", path.Base(mf.FilePath()), err)) + continue + } + mfBytes, err := s.readFilerFileContent(ctx, mfPath) + if err != nil { + notes = append(notes, fmt.Sprintf("Failed to read manifest %s: %v", path.Base(mf.FilePath()), err)) + continue + } + entries, err := iceberg.ReadManifest(mf, bytes.NewReader(mfBytes), true) + if err != nil { + notes = append(notes, fmt.Sprintf("Failed to parse manifest %s: %v", path.Base(mf.FilePath()), err)) + continue + } + for _, entry := range entries { + df := entry.DataFile() + files = append(files, IcebergDataFileInfo{ + Path: df.FilePath(), + Format: string(df.FileFormat()), + RecordCount: df.Count(), + SizeBytes: df.FileSizeBytes(), + }) + } + } + return files, hasDeletes, notes, nil +} + +// icebergLocationToFilerPath maps an Iceberg file location (s3://bucket/..., +// an absolute filer path, or a table-relative path like data/x.parquet) to a +// filer path, which must stay under the table-buckets root. +func icebergLocationToFilerPath(location, bucketName, namespaceDir, tableName string) (string, error) { + p := location + if idx := strings.Index(p, "://"); idx >= 0 { + p = path.Join(s3tables.TablesPath, p[idx+3:]) + } else if strings.HasPrefix(p, "/") { + p = path.Clean(p) + } else { + tableDir := path.Join(s3tables.TablesPath, bucketName, namespaceDir, tableName) + p = path.Join(tableDir, p) + if !strings.HasPrefix(p, tableDir+"/") { + return "", fmt.Errorf("location %q resolves outside the table directory", location) + } + } + p = path.Clean(p) + if !strings.HasPrefix(p, s3tables.TablesPath+"/") { + return "", fmt.Errorf("location %q resolves outside %s", location, s3tables.TablesPath) + } + return p, nil +} + +func (s *AdminServer) lookupFilerEntry(ctx context.Context, fullPath string) (*filer_pb.Entry, error) { + dir, name := path.Dir(fullPath), path.Base(fullPath) + var entry *filer_pb.Entry + err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + resp, err := filer_pb.LookupEntry(ctx, client, &filer_pb.LookupDirectoryEntryRequest{ + Directory: dir, + Name: name, + }) + if err != nil { + return err + } + entry = resp.Entry + return nil + }) + if err != nil { + return nil, err + } + if entry == nil { + return nil, fmt.Errorf("not found: %s", fullPath) + } + return entry, nil +} + +func (s *AdminServer) readFilerFileContent(ctx context.Context, fullPath string) ([]byte, error) { + entry, err := s.lookupFilerEntry(ctx, fullPath) + if err != nil { + return nil, err + } + if len(entry.Content) > 0 || len(entry.GetChunks()) == 0 { + return entry.Content, nil + } + size := int64(filer.FileSize(entry)) + if size > icebergPreviewMaxMetaBytes { + return nil, fmt.Errorf("file too large to load (%d bytes)", size) + } + streamFn, err := filer.PrepareStreamContentWithThrottler(ctx, s.GetMasterClient(), VolumeServerReadJwt, entry.GetChunks(), 0, size, 0) + if err != nil { + return nil, err + } + var buf bytes.Buffer + if err := streamFn(&buf); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func (s *AdminServer) readParquetRows(ctx context.Context, fullPath string, want int) ([]string, [][]string, error) { + entry, err := s.lookupFilerEntry(ctx, fullPath) + if err != nil { + return nil, nil, err + } + var readerAt io.ReaderAt + var size int64 + if len(entry.Content) > 0 || len(entry.GetChunks()) == 0 { + readerAt = bytes.NewReader(entry.Content) + size = int64(len(entry.Content)) + } else { + size = int64(filer.FileSize(entry)) + readerAt = &filerChunkReaderAt{ctx: ctx, server: s, chunks: entry.GetChunks(), size: size} + } + + pf, err := parquet.OpenFile(readerAt, size, parquet.SkipPageIndex(true), parquet.SkipBloomFilters(true)) + if err != nil { + return nil, nil, fmt.Errorf("open parquet: %w", err) + } + leafColumns := pf.Schema().Columns() + cols := make([]string, len(leafColumns)) + for i, c := range leafColumns { + cols[i] = strings.Join(c, ".") + } + + reader := parquet.NewReader(pf) + defer reader.Close() + rowBuf := make([]parquet.Row, 32) + var out [][]string + for len(out) < want { + n, readErr := reader.ReadRows(rowBuf) + for i := 0; i < n && len(out) < want; i++ { + out = append(out, formatParquetRow(rowBuf[i], len(cols))) + } + if readErr != nil { + if readErr == io.EOF { + break + } + return nil, nil, readErr + } + if n == 0 { + break + } + } + return cols, out, nil +} + +func formatParquetRow(row parquet.Row, columnCount int) []string { + cells := make([]string, columnCount) + seen := make([]bool, columnCount) + for _, v := range row { + ci := v.Column() + if ci < 0 || ci >= columnCount { + continue + } + cell := formatParquetValue(v) + if seen[ci] { + cells[ci] += ", " + cell + } else { + cells[ci] = cell + seen[ci] = true + } + } + return cells +} + +func formatParquetValue(v parquet.Value) string { + if v.IsNull() { + return "" + } + switch v.Kind() { + case parquet.ByteArray, parquet.FixedLenByteArray: + b := v.ByteArray() + if utf8.Valid(b) { + return truncateCell(string(b)) + } + return truncateCell(fmt.Sprintf("0x%x", b)) + default: + return truncateCell(v.String()) + } +} + +func truncateCell(s string) string { + if len(s) <= icebergPreviewMaxCellChars { + return s + } + cut := icebergPreviewMaxCellChars + for cut > 0 && !utf8.RuneStart(s[cut]) { + cut-- + } + return s[:cut] + "…" +} + +// filerChunkReaderAt serves random-access reads over a chunked filer entry by +// issuing ranged volume-server reads, carrying the read JWT when configured. +type filerChunkReaderAt struct { + ctx context.Context + server *AdminServer + chunks []*filer_pb.FileChunk + size int64 +} + +func (r *filerChunkReaderAt) ReadAt(p []byte, off int64) (int, error) { + if off < 0 { + return 0, fmt.Errorf("negative offset: %d", off) + } + if off >= r.size { + return 0, io.EOF + } + want := int64(len(p)) + if off+want > r.size { + want = r.size - off + } + streamFn, err := filer.PrepareStreamContentWithThrottler(r.ctx, r.server.GetMasterClient(), VolumeServerReadJwt, r.chunks, off, want, 0) + if err != nil { + return 0, err + } + // Write straight into p; a bytes.Buffer wrapping p would silently + // allocate a fresh backing array if it ever grew, dropping bytes the + // caller expects in p. + w := &sliceWriter{dst: p} + if err := streamFn(w); err != nil { + return w.n, err + } + if w.n < len(p) { + return w.n, io.EOF + } + return w.n, nil +} + +// sliceWriter writes into a fixed destination slice, discarding any overflow. +type sliceWriter struct { + dst []byte + n int +} + +func (w *sliceWriter) Write(p []byte) (int, error) { + c := copy(w.dst[w.n:], p) + w.n += c + return c, nil +} diff --git a/weed/admin/dash/iceberg_data_preview_test.go b/weed/admin/dash/iceberg_data_preview_test.go new file mode 100644 index 000000000..b96c0ac87 --- /dev/null +++ b/weed/admin/dash/iceberg_data_preview_test.go @@ -0,0 +1,136 @@ +package dash + +import ( + "bytes" + "io" + "strings" + "testing" + + "github.com/parquet-go/parquet-go" +) + +func TestIcebergLocationToFilerPath(t *testing.T) { + tests := []struct { + location string + want string + wantErr bool + }{ + {location: "s3://warehouse/ns/tbl/data/f1.parquet", want: "/buckets/warehouse/ns/tbl/data/f1.parquet"}, + {location: "s3a://warehouse/ns/tbl/metadata/snap-1.avro", want: "/buckets/warehouse/ns/tbl/metadata/snap-1.avro"}, + {location: "/buckets/warehouse/ns/tbl/data/f1.parquet", want: "/buckets/warehouse/ns/tbl/data/f1.parquet"}, + {location: "data/f1.parquet", want: "/buckets/warehouse/ns/tbl/data/f1.parquet"}, + {location: "metadata/v3.metadata.json", want: "/buckets/warehouse/ns/tbl/metadata/v3.metadata.json"}, + {location: "s3://warehouse/../../etc/passwd", wantErr: true}, + {location: "/etc/passwd", wantErr: true}, + {location: "../../../etc/passwd", wantErr: true}, + } + for _, tc := range tests { + got, err := icebergLocationToFilerPath(tc.location, "warehouse", "ns", "tbl") + if tc.wantErr { + if err == nil { + t.Errorf("icebergLocationToFilerPath(%q) = %q, want error", tc.location, got) + } + continue + } + if err != nil { + t.Errorf("icebergLocationToFilerPath(%q): %v", tc.location, err) + continue + } + if got != tc.want { + t.Errorf("icebergLocationToFilerPath(%q) = %q, want %q", tc.location, got, tc.want) + } + } +} + +func TestFormatParquetRow(t *testing.T) { + type record struct { + ID int64 `parquet:"id"` + Name string `parquet:"name"` + Score float64 `parquet:"score"` + Blob []byte `parquet:"blob"` + Note *string `parquet:"note,optional"` + } + + var buf bytes.Buffer + w := parquet.NewGenericWriter[record](&buf) + note := "hello" + if _, err := w.Write([]record{ + {ID: 1, Name: "alpha", Score: 1.5, Blob: []byte{0xff, 0xfe}, Note: ¬e}, + {ID: 2, Name: "beta", Score: -2.25, Blob: []byte("text"), Note: nil}, + }); err != nil { + t.Fatal(err) + } + if err := w.Close(); err != nil { + t.Fatal(err) + } + + pf, err := parquet.OpenFile(bytes.NewReader(buf.Bytes()), int64(buf.Len())) + if err != nil { + t.Fatal(err) + } + leafColumns := pf.Schema().Columns() + cols := make([]string, len(leafColumns)) + for i, c := range leafColumns { + cols[i] = strings.Join(c, ".") + } + + reader := parquet.NewReader(pf) + defer reader.Close() + rows := make([]parquet.Row, 4) + n, err := reader.ReadRows(rows) + if err != nil && err != io.EOF { + t.Fatal(err) + } + if n != 2 { + t.Fatalf("got %d rows, want 2", n) + } + + byName := func(cells []string, name string) string { + for i, col := range cols { + if col == name { + return cells[i] + } + } + t.Fatalf("column %q not found in %v", name, cols) + return "" + } + + first := formatParquetRow(rows[0], len(cols)) + if got := byName(first, "id"); got != "1" { + t.Errorf("id = %q, want 1", got) + } + if got := byName(first, "name"); got != "alpha" { + t.Errorf("name = %q, want alpha", got) + } + if got := byName(first, "score"); got != "1.5" { + t.Errorf("score = %q, want 1.5", got) + } + if got := byName(first, "blob"); got != "0xfffe" { + t.Errorf("blob = %q, want 0xfffe", got) + } + if got := byName(first, "note"); got != "hello" { + t.Errorf("note = %q, want hello", got) + } + + second := formatParquetRow(rows[1], len(cols)) + if got := byName(second, "blob"); got != "text" { + t.Errorf("blob = %q, want text", got) + } + if got := byName(second, "note"); got != "" { + t.Errorf("note = %q, want empty for null", got) + } +} + +func TestTruncateCell(t *testing.T) { + long := strings.Repeat("界", 200) + got := truncateCell(long) + if len(got) > icebergPreviewMaxCellChars+len("…") { + t.Errorf("truncated cell too long: %d", len(got)) + } + if !strings.HasSuffix(got, "…") { + t.Errorf("truncated cell should end with ellipsis") + } + if short := truncateCell("abc"); short != "abc" { + t.Errorf("short cell changed: %q", short) + } +} diff --git a/weed/admin/handlers/admin_handlers.go b/weed/admin/handlers/admin_handlers.go index 2ed2a8d4f..1437d0c2d 100644 --- a/weed/admin/handlers/admin_handlers.go +++ b/weed/admin/handlers/admin_handlers.go @@ -114,6 +114,7 @@ func (h *AdminHandlers) registerUIRoutes(r *mux.Router) { r.HandleFunc("/object-store/s3tables/buckets/{bucket}/namespaces", h.ShowS3TablesNamespaces).Methods(http.MethodGet) r.HandleFunc("/object-store/s3tables/buckets/{bucket}/namespaces/{namespace}/tables", h.ShowS3TablesTables).Methods(http.MethodGet) r.HandleFunc("/object-store/s3tables/buckets/{bucket}/namespaces/{namespace}/tables/{table}", h.ShowS3TablesTableDetails).Methods(http.MethodGet) + r.HandleFunc("/object-store/s3tables/buckets/{bucket}/namespaces/{namespace}/tables/{table}/data", h.ShowS3TablesTableData).Methods(http.MethodGet) r.HandleFunc("/object-store/iceberg", h.ShowIcebergCatalog).Methods(http.MethodGet) r.HandleFunc("/object-store/iceberg/{catalog}/namespaces", h.ShowIcebergNamespaces).Methods(http.MethodGet) r.HandleFunc("/object-store/iceberg/{catalog}/namespaces/{namespace}/tables", h.ShowIcebergTables).Methods(http.MethodGet) @@ -442,6 +443,38 @@ func (h *AdminHandlers) ShowS3TablesTableDetails(w http.ResponseWriter, r *http. } } +// ShowS3TablesTableData renders sample rows and the data-file list of an Iceberg table snapshot. +func (h *AdminHandlers) ShowS3TablesTableData(w http.ResponseWriter, r *http.Request) { + bucketName := mux.Vars(r)["bucket"] + namespace := mux.Vars(r)["namespace"] + tableName := mux.Vars(r)["table"] + arn, err := buildS3TablesBucketArn(bucketName) + if err != nil { + writeJSONError(w, http.StatusBadRequest, err.Error()) + return + } + + query := r.URL.Query() + snapshotID, _ := strconv.ParseInt(query.Get("snapshot"), 10, 64) + limit, _ := strconv.Atoi(query.Get("limit")) + + username := h.getUsername(r) + data, err := h.adminServer.GetIcebergTableDataPreview(r.Context(), bucketName, arn, namespace, tableName, snapshotID, query.Get("file"), limit) + if err != nil { + writeJSONError(w, http.StatusInternalServerError, "Failed to get table data: "+err.Error()) + return + } + data.Username = username + + w.Header().Set("Content-Type", "text/html") + component := app.IcebergTableData(data) + viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context())) + layoutComponent := layout.Layout(viewCtx, component) + if err := layoutComponent.Render(r.Context(), w); err != nil { + writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error()) + } +} + func buildS3TablesBucketArn(bucketName string) (string, error) { return s3tables.BuildBucketARN(s3tables.DefaultRegion, s3_constants.AccountAdminId, bucketName) } diff --git a/weed/admin/handlers/file_browser_grpc.go b/weed/admin/handlers/file_browser_grpc.go index 1ba21c53f..a79e84d7e 100644 --- a/weed/admin/handlers/file_browser_grpc.go +++ b/weed/admin/handlers/file_browser_grpc.go @@ -18,7 +18,6 @@ import ( "github.com/seaweedfs/seaweedfs/weed/operation" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/security" - "github.com/seaweedfs/seaweedfs/weed/util" ) // Admin upload chunk size, matching s3api so files split into the same fid-sized pieces. @@ -256,7 +255,7 @@ func (h *FileBrowserHandlers) streamEntryContent(ctx context.Context, entry *fil streamFn, err := filer.PrepareStreamContentWithThrottler( ctx, h.adminServer.GetMasterClient(), - volumeServerReadJwt, + dash.VolumeServerReadJwt, entry.GetChunks(), 0, size, @@ -267,17 +266,3 @@ func (h *FileBrowserHandlers) streamEntryContent(ctx context.Context, entry *fil } return streamFn(w) } - -// volumeServerReadJwt mints a per-fileId Bearer token for reads against a -// volume server when jwt.signing.read.key is configured. The volume servers -// are unaware of jwt.filer_signing.read.key — that one only gates the filer -// HTTP surface, which this code path doesn't touch. -func volumeServerReadJwt(fileId string) string { - v := util.GetViper() - signingKey := security.SigningKey(v.GetString("jwt.signing.read.key")) - if len(signingKey) == 0 { - return "" - } - expiresAfterSec := v.GetInt("jwt.signing.read.expires_after_seconds") - return string(security.GenJwtForVolumeServer(signingKey, expiresAfterSec, fileId)) -} diff --git a/weed/admin/view/app/iceberg_table_data.templ b/weed/admin/view/app/iceberg_table_data.templ new file mode 100644 index 000000000..1fefcd06a --- /dev/null +++ b/weed/admin/view/app/iceberg_table_data.templ @@ -0,0 +1,258 @@ +package app + +import ( + "fmt" + "net/url" + "path" + + "github.com/seaweedfs/seaweedfs/weed/admin/dash" +) + +templ IcebergTableData(data dash.IcebergDataPreviewData) { +
+

+ +

+
+ +
+
+ if data.PreviewError != "" { +
+ { data.PreviewError } +
+ } + for _, note := range data.PreviewNotes { +
+ { note } +
+ } +
+
+
+
+
+
+
+ Snapshot +
+
+ if data.SnapshotID != 0 { + { fmt.Sprintf("%d", data.SnapshotID) } + if data.SnapshotID == data.CurrentSnapshotID { + current + } + } else { + - + } +
+ if !data.SnapshotTime.IsZero() { +
{ data.SnapshotTime.Format("2006-01-02 15:04:05") }
+ } +
+
+ if len(data.Snapshots) > 1 { + + } else { + + } +
+
+
+
+
+
+
+
+
+
+
+ Data Files +
+
+ { formatNumber(int64(data.TotalDataFiles)) } +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Total Records +
+
+ { formatNumber(data.TotalRecords) } +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Sample Rows + if data.SelectedFile != "" { + { path.Base(data.SelectedFile) } + all files + } +
+
+ for _, limit := range []int{25, 50, 100, 200} { + if limit == data.RowLimit { + { fmt.Sprintf("%d", limit) } + } else { + { fmt.Sprintf("%d", limit) } + } + } +
+
+
+ if len(data.Columns) > 0 { +
+ + + + + for _, col := range data.Columns { + + } + + + + for i, row := range data.Rows { + + + for _, cell := range row { + + } + + } + +
#{ col }
{ fmt.Sprintf("%d", i+1) }{ cell }
+
+
+ { fmt.Sprintf("Showing %d row(s) from %d data file(s).", len(data.Rows), data.ScannedFiles) } +
+ } else { +
No rows to preview.
+ } +
+
+
+
+
+
+
+
+
+ Data Files +
+
+
+
+ + + + + + + + + + + + for _, file := range data.DataFiles { + + + + + + + + } + if len(data.DataFiles) == 0 { + + + + } + +
PathFormatRecordsSize
{ file.Path }{ file.Format }{ formatNumber(file.RecordCount) }{ formatBytes(file.SizeBytes) } + + Preview + +
No data files in this snapshot.
+
+
+
+
+
+} diff --git a/weed/admin/view/app/iceberg_table_data_templ.go b/weed/admin/view/app/iceberg_table_data_templ.go new file mode 100644 index 000000000..91ef699f5 --- /dev/null +++ b/weed/admin/view/app/iceberg_table_data_templ.go @@ -0,0 +1,664 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1001 +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" + "net/url" + "path" + + "github.com/seaweedfs/seaweedfs/weed/admin/dash" +) + +func IcebergTableData(data dash.IcebergDataPreviewData) 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.PreviewError != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.PreviewError) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 50, Col: 70} + } + _, 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, 11, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + for _, note := range data.PreviewNotes { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(note) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 55, Col: 48} + } + _, 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, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
Snapshot
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.SnapshotID != 0 { + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.SnapshotID)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 69, Col: 45} + } + _, 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, 15, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.SnapshotID == data.CurrentSnapshotID { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "current") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } else { + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !data.SnapshotTime.IsZero() { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(data.SnapshotTime.Format("2006-01-02 15:04:05")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 78, Col: 87} + } + _, 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, 20, "
") + 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 + } + if len(data.Snapshots) > 1 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
Data Files
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(formatNumber(int64(data.TotalDataFiles))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 127, Col: 50} + } + _, 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, 35, "
Total Records
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(formatNumber(data.TotalRecords)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 146, Col: 41} + } + _, 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, 36, "
Sample Rows ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.SelectedFile != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(path.Base(data.SelectedFile)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 164, Col: 75} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " all files") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, limit := range []int{25, 50, 100, 200} { + if limit == data.RowLimit { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", limit)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 171, Col: 203} + } + _, 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, 43, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var26 string + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", limit)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 173, Col: 211} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(data.Columns) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, col := range data.Columns { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for i, row := range data.Rows { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, cell := range row { + 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, 56, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "
#") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var27 string + templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(col) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 186, Col: 20} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var28 string + templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i+1)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 193, Col: 58} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var29 string + templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(cell) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 195, Col: 42} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var30 string + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Showing %d row(s) from %d data file(s).", len(data.Rows), data.ScannedFiles)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 203, Col: 98} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
No rows to preview.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "
Data Files
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, file := range data.DataFiles { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.DataFiles) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "
PathFormatRecordsSize
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var31 string + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(file.Path) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 235, Col: 45} + } + _, 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, 62, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var32 string + templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(file.Format) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 236, Col: 60} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var33 string + templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(formatNumber(file.RecordCount)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 237, Col: 46} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var34 string + templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(file.SizeBytes)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_data.templ`, Line: 238, Col: 43} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "Preview
No data files in this snapshot.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/weed/admin/view/app/iceberg_table_data_urls.go b/weed/admin/view/app/iceberg_table_data_urls.go new file mode 100644 index 000000000..a106ceda5 --- /dev/null +++ b/weed/admin/view/app/iceberg_table_data_urls.go @@ -0,0 +1,28 @@ +package app + +import ( + "net/url" + "strconv" +) + +// icebergTableDataURL builds the table data-preview page URL. Zero snapshotID +// means the current snapshot; zero limit means the server default. +func icebergTableDataURL(catalog, namespace, table string, snapshotID int64, limit int, file string) string { + u := "/object-store/s3tables/buckets/" + url.PathEscape(catalog) + + "/namespaces/" + url.PathEscape(namespace) + + "/tables/" + url.PathEscape(table) + "/data" + q := url.Values{} + if snapshotID != 0 { + q.Set("snapshot", strconv.FormatInt(snapshotID, 10)) + } + if limit > 0 { + q.Set("limit", strconv.Itoa(limit)) + } + if file != "" { + q.Set("file", file) + } + if len(q) > 0 { + u += "?" + q.Encode() + } + return u +} diff --git a/weed/admin/view/app/iceberg_table_details.templ b/weed/admin/view/app/iceberg_table_details.templ index d2ed5cf41..b6a286a19 100644 --- a/weed/admin/view/app/iceberg_table_details.templ +++ b/weed/admin/view/app/iceberg_table_details.templ @@ -32,6 +32,11 @@ templ IcebergTableDetails(data dash.IcebergTableDetailsData) {
+
+ + Browse Data + +
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" data-catalog-name=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(data.CatalogName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 41, Col: 225} + } + _, 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, 12, "\">Drop Table") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.MetadataError != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(data.MetadataError) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 44, Col: 71} - } - _, 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, 13, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
Data Files
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if data.HasDataFileCount { var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(formatNumber(data.DataFileCount)) + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(data.MetadataError) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 58, Col: 43} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 49, Col: 71} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "-") + 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, 16, "
Total Size
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
Data Files
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if data.HasTotalSize { + if data.HasDataFileCount { var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(data.TotalSizeBytes)) + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(formatNumber(data.DataFileCount)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 81, Col: 43} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 63, Col: 43} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) 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, 16, "-") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
Snapshots
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
Total Size
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if data.HasSnapshotCount { + if data.HasTotalSize { var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(formatNumber(int64(data.SnapshotCount))) + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(data.TotalSizeBytes)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 104, Col: 50} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 86, Col: 43} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "-") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "-") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
Table Metadata
Table ARN") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
Snapshots
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(data.TableARN) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 131, Col: 52} + if data.HasSnapshotCount { + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(formatNumber(int64(data.SnapshotCount))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 109, Col: 50} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "-") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
Format") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
Table Metadata
Table ARN") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(data.Format) + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(data.TableARN) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 135, Col: 25} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 136, Col: 52} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
Table Location") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
Format") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(data.Format) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 140, Col: 25} + } + _, 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, 23, "
Table Location") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.TableLocation != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(data.TableLocation) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 141, Col: 50} - } - _, 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, 24, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "-") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
Metadata Location") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if data.MetadataLocation != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var19 string - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(data.MetadataLocation) + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(data.TableLocation) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 151, Col: 53} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 146, Col: 50} } _, 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, 28, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "-") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "-") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
Created") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
Metadata Location") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if !data.CreatedAt.IsZero() { - var templ_7745c5c3_Var20 string - templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(data.CreatedAt.Format("2006-01-02 15:04")) + if data.MetadataLocation != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 161, Col: 53} + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(data.MetadataLocation) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 156, Col: 53} } _, 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, 29, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "-") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "-") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
Modified") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
Created") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if !data.ModifiedAt.IsZero() { + if !data.CreatedAt.IsZero() { var templ_7745c5c3_Var21 string - templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(data.ModifiedAt.Format("2006-01-02 15:04")) + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(data.CreatedAt.Format("2006-01-02 15:04")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 171, Col: 54} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 166, Col: 53} } _, 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, 33, "-") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "-") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
Properties
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
KeyValue
Modified") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - for _, prop := range data.Properties { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } + if !data.ModifiedAt.IsZero() { var templ_7745c5c3_Var22 string - templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(prop.Key) + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(data.ModifiedAt.Format("2006-01-02 15:04")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 200, Col: 23} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 176, Col: 54} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "-") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
Properties
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, prop := range data.Properties { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - if len(data.Properties) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
KeyValue
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(prop.Value) + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(prop.Key) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 201, Col: 25} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 205, Col: 23} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
No properties available.
Schema
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, field := range data.SchemaFields { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.Properties) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
IDNameTypeRequired
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var24 string - templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", field.ID)) + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(prop.Value) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 237, Col: 43} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 206, Col: 25} } _, 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, 41, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
No properties available.
Schema
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, field := range data.SchemaFields { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - if len(data.SchemaFields) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "
IDNameTypeRequired
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var25 string - templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(field.Name) + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", field.ID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 238, Col: 26} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 242, Col: 43} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) 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, 42, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var26 string - templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(string(field.Type)) + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(field.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 239, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 243, Col: 26} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if field.Required { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "Yes") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "No") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "
No schema available.
Partitions
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, field := range data.PartitionFields { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.SchemaFields) == 0 { + 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, 49, "
NameTransformSource IDField ID
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var27 string - templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(field.Name) + templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(string(field.Type)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 283, Col: 26} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 244, Col: 40} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) 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, 44, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if field.Required { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "Yes") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "No") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "
No schema available.
Partitions
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, field := range data.PartitionFields { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - if len(data.PartitionFields) == 0 { - 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, 55, "
NameTransformSource IDField ID
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var28 string - templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(field.Transform) + templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(field.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 284, Col: 31} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 288, Col: 26} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) if templ_7745c5c3_Err != nil { @@ -519,9 +519,9 @@ func IcebergTableDetails(data dash.IcebergTableDetailsData) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", field.SourceID)) + templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(field.Transform) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 285, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 289, Col: 31} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) if templ_7745c5c3_Err != nil { @@ -532,123 +532,136 @@ func IcebergTableDetails(data dash.IcebergTableDetailsData) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", field.FieldID)) + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", field.SourceID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 286, Col: 48} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 290, Col: 49} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
No partitions defined.
Snapshot History
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, snapshot := range data.Snapshots { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.PartitionFields) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "
Snapshot IDTimestampOperationManifest List
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var31 string - templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", snapshot.SnapshotID)) + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", field.FieldID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 323, Col: 54} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 291, Col: 48} } _, 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, 57, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
No partitions defined.
Snapshot History
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, snapshot := range data.Snapshots { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if len(data.Snapshots) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "
Snapshot IDTimestampOperationManifest List
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var32 string + templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", snapshot.SnapshotID)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 328, Col: 54} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if snapshot.Timestamp.IsZero() { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "-") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "-") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - var templ_7745c5c3_Var32 string - templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(snapshot.Timestamp.Format("2006-01-02 15:04")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 328, Col: 59} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if snapshot.Operation != "" { var templ_7745c5c3_Var33 string - templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(snapshot.Operation) + templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(snapshot.Timestamp.Format("2006-01-02 15:04")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 333, Col: 32} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 333, Col: 59} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "-") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if snapshot.ManifestList != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } + if snapshot.Operation != "" { var templ_7745c5c3_Var34 string - templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(snapshot.ManifestList) + templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(snapshot.Operation) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 340, Col: 55} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 338, Col: 32} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "-") + 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, 65, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if snapshot.ManifestList != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var35 string + templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(snapshot.ManifestList) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/iceberg_table_details.templ`, Line: 345, Col: 55} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "-") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "
No snapshots available.
No snapshots available.
Drop Table

Are you sure you want to drop the table ?

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "
Drop Table

Are you sure you want to drop the table ?

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }