This is an automated email from the ASF dual-hosted git repository.

chenli pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new 8635ce2477 feat(ui): add pagination and search for wide-column tables 
(#4086)
8635ce2477 is described below

commit 8635ce247772109afb81e84d49d5ec1e480a01c4
Author: Ma77Ball <[email protected]>
AuthorDate: Sat Jan 3 10:11:51 2026 -0800

    feat(ui): add pagination and search for wide-column tables (#4086)
    
    <!--
    Thanks for sending a pull request (PR)! Here are some tips for you:
    1. If this is your first time, please read our contributor guidelines:
    [Contributing to
    Texera](https://github.com/apache/texera/blob/main/CONTRIBUTING.md)
      2. Ensure you have added or run the appropriate tests for your PR
      3. If the PR is a work in progress, mark it as a draft on GitHub.
      4. Please write your PR title to summarize what this PR proposes. We
        are following the Conventional Commits style for PR titles as well.
      5. Be sure to keep the PR description updated to reflect all changes.
    -->
    
    ### What changes were proposed in this PR?
    <!--
    Please clarify what changes you are proposing. The purpose of this
    section
    is to outline the changes. Here are some tips for you:
      1. If you propose a new API, clarify the use case for a new API.
      2. If you fix a bug, you can clarify why it is a bug.
      3. If it is a refactoring, could you explain what has been changed?
      3. It would be helpful to include a before-and-after comparison using
         screenshots or GIFs.
    4. Please consider writing helpful notes for better and faster reviews.
    -->
    This PR adds a feature that enables Texera to efficiently handle tables
    with vast numbers of columns in the result panel.
    
    This PR adds UI features that enable Texera to efficiently handle tables
    with large numbers of columns in the result viewer and related data
    preview components.
    
    Specifically, this PR introduces:
    
    1. Horizontal Column Pagination
        1. "Next Columns" and "Previous Columns" buttons have been added.
    2. Columns are now loaded in column windows (configurable size, default
    25).
    3. Prevents UI freezing or overflow when dealing with tables containing
    hundreds or thousands of columns.
    2. Column Search Bar
    1. A new search box allows users to filter or jump directly to column
    names.
    2. When a column is found, that column window is automatically loaded
    and highlighted.
        3. Useful for wide schemas such as:
            1. large scientific datasets
            2. logs with hundreds of attributes
            3. denormalized tables or wide joins
    3. Improvements to rendering performance
        1. The frontend now only renders the visible subset of columns.
        2. Reduces DOM load and improves React change detection speed.
    
    ### Any related issues, documentation, or discussions?
    <!--
    Please use this section to link to other resources if not mentioned
    already.
    1. If this PR fixes an issue, please include `Fixes #1234`, `Resolves
    #1234.`
         or `Closes #1234`. If it is only related, mention the issue number.
      2. If there is design documentation, please add the link.
      3. If there is a discussion on the mailing list, please add the link.
    -->
    Fixes: https://github.com/apache/texera/issues/3825
    
    ### How was this PR tested?
    <!--
    If tests were added, say so here. Or mention that if the PR
    is tested with existing test cases. Please include/update test cases
    that
    check the changes thoroughly, including negative and positive cases if
    possible.
    If it was tested in a way different from regular unit tests, please
    clarify how
    you tested step by step, ideally copy and paste-able, so that other
    reviewers can
    test and check, and descendants can verify in the future. If tests were
    not added,
    please describe why they were not added and/or why it was difficult to
    add.
    -->
    
    1. Ran a couple of CSV scan operators that produced a  wide output table
    2. Clicked through column windows in both directions
    3. Using search to jump to:
        - first column
        - random middle column
        - last column
    
    ### Was this PR authored or co-authored using generative AI tooling?
    <!--
    If generative AI tooling has been used in the process of authoring this
    PR,
    please include the phrase: 'Generated-by: ' followed by the name of the
    tool
    and its version. If no, write 'No'.
    Please refer to the [ASF Generative Tooling
    Guidance](https://www.apache.org/legal/generative-tooling.html) for
    details.
    -->
    No
    
    ### New Layout:
    <img width="1905" height="733" alt="image"
    
src="https://github.com/user-attachments/assets/18d14c2d-c134-422f-a5d1-2c826cf3a8e9";
    />
    
    ---------
    
    Co-authored-by: Chen Li <[email protected]>
    Co-authored-by: Chris <[email protected]>
    Co-authored-by: Xinyuan Lin <[email protected]>
---
 .../request/ResultPaginationRequest.scala          |  5 +-
 .../web/service/ExecutionResultService.scala       | 23 ++++++--
 common/config/src/main/resources/gui.conf          |  4 ++
 .../scala/org/apache/texera/config/GuiConfig.scala |  2 +
 .../amber/core/storage/DocumentFactory.scala       |  8 +--
 .../storage/model/ReadonlyLocalFileDocument.scala  |  6 ++-
 .../storage/model/ReadonlyVirtualDocument.scala    |  3 +-
 .../amber/core/storage/model/VirtualDocument.scala |  3 +-
 .../storage/result/iceberg/IcebergDocument.scala   | 23 ++++++--
 .../texera/service/resource/ConfigResource.scala   |  1 +
 .../app/common/service/gui-config.service.mock.ts  |  1 +
 frontend/src/app/common/type/gui-config.ts         |  1 +
 .../result-panel/result-panel.component.ts         |  2 +-
 .../result-table-frame.component.html              | 34 ++++++++++++
 .../result-table-frame.component.spec.ts           | 13 +++++
 .../result-table-frame.component.ts                | 63 ++++++++++++++++++++--
 .../workflow-result/workflow-result.service.ts     | 14 ++++-
 .../types/workflow-websocket.interface.ts          |  3 ++
 18 files changed, 185 insertions(+), 24 deletions(-)

diff --git 
a/amber/src/main/scala/org/apache/texera/web/model/websocket/request/ResultPaginationRequest.scala
 
b/amber/src/main/scala/org/apache/texera/web/model/websocket/request/ResultPaginationRequest.scala
index 32649c75de..4a3e0a58a3 100644
--- 
a/amber/src/main/scala/org/apache/texera/web/model/websocket/request/ResultPaginationRequest.scala
+++ 
b/amber/src/main/scala/org/apache/texera/web/model/websocket/request/ResultPaginationRequest.scala
@@ -23,5 +23,8 @@ case class ResultPaginationRequest(
     requestID: String,
     operatorID: String,
     pageIndex: Int,
-    pageSize: Int
+    pageSize: Int,
+    columnOffset: Int = 0,
+    columnLimit: Int = Int.MaxValue,
+    columnSearch: Option[String] = None
 ) extends TexeraWebSocketRequest
diff --git 
a/amber/src/main/scala/org/apache/texera/web/service/ExecutionResultService.scala
 
b/amber/src/main/scala/org/apache/texera/web/service/ExecutionResultService.scala
index b90de7e01f..8810e9891f 100644
--- 
a/amber/src/main/scala/org/apache/texera/web/service/ExecutionResultService.scala
+++ 
b/amber/src/main/scala/org/apache/texera/web/service/ExecutionResultService.scala
@@ -437,12 +437,25 @@ class ExecutionResultService(
 
     storageUriOption match {
       case Some(storageUri) =>
+        val (document, schemaOption) = DocumentFactory.openDocument(storageUri)
+        val virtualDocument = document.asInstanceOf[VirtualDocument[Tuple]]
+
+        val columns = {
+          val schema = schemaOption.get
+          val allColumns = schema.getAttributeNames
+          val filteredColumns = request.columnSearch match {
+            case Some(search) =>
+              allColumns.filter(col => 
col.toLowerCase.contains(search.toLowerCase))
+            case None => allColumns
+          }
+          Some(
+            filteredColumns.slice(request.columnOffset, request.columnOffset + 
request.columnLimit)
+          )
+        }
+
         val paginationIterable = {
-          DocumentFactory
-            .openDocument(storageUri)
-            ._1
-            .asInstanceOf[VirtualDocument[Tuple]]
-            .getRange(from, from + request.pageSize)
+          virtualDocument
+            .getRange(from, from + request.pageSize, columns)
             .to(Iterable)
         }
         val mappedResults = convertTuplesToJson(paginationIterable)
diff --git a/common/config/src/main/resources/gui.conf 
b/common/config/src/main/resources/gui.conf
index f73cba82c3..8039441b13 100644
--- a/common/config/src/main/resources/gui.conf
+++ b/common/config/src/main/resources/gui.conf
@@ -108,5 +108,9 @@ gui {
     # whether AI copilot feature is enabled
     copilot-enabled = false
     copilot-enabled = ${?GUI_WORKFLOW_WORKSPACE_COPILOT_ENABLED}
+
+    # the limit of columns to be displayed in the result table
+    limit-columns = 15
+    limit-columns = ${?GUI_WORKFLOW_WORKSPACE_LIMIT_COLUMNS}
   }
 }
\ No newline at end of file
diff --git 
a/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala 
b/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala
index 5e16529bb6..14016f4374 100644
--- a/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala
+++ b/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala
@@ -69,4 +69,6 @@ object GuiConfig {
     conf.getInt("gui.workflow-workspace.active-time-in-minutes")
   val guiWorkflowWorkspaceCopilotEnabled: Boolean =
     conf.getBoolean("gui.workflow-workspace.copilot-enabled")
+  val guiWorkflowWorkspaceLimitColumns: Int =
+    conf.getInt("gui.workflow-workspace.limit-columns")
 }
diff --git 
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/DocumentFactory.scala
 
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/DocumentFactory.scala
index a828ab12b7..4c37c33bb2 100644
--- 
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/DocumentFactory.scala
+++ 
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/DocumentFactory.scala
@@ -87,8 +87,8 @@ object DocumentFactory {
               overrideIfExists = true
             )
             val serde: (IcebergSchema, Tuple) => Record = 
IcebergUtil.toGenericRecord
-            val deserde: (IcebergSchema, Record) => Tuple = (_, record) =>
-              IcebergUtil.fromRecord(record, schema)
+            val deserde: (IcebergSchema, Record) => Tuple = (schema, record) =>
+              IcebergUtil.fromRecord(record, 
IcebergUtil.fromIcebergSchema(schema))
 
             new IcebergDocument[Tuple](
               namespace,
@@ -144,8 +144,8 @@ object DocumentFactory {
 
             val amberSchema = IcebergUtil.fromIcebergSchema(table.schema())
             val serde: (IcebergSchema, Tuple) => Record = 
IcebergUtil.toGenericRecord
-            val deserde: (IcebergSchema, Record) => Tuple = (_, record) =>
-              IcebergUtil.fromRecord(record, amberSchema)
+            val deserde: (IcebergSchema, Record) => Tuple = (schema, record) =>
+              IcebergUtil.fromRecord(record, 
IcebergUtil.fromIcebergSchema(schema))
 
             (
               new IcebergDocument[Tuple](
diff --git 
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/ReadonlyLocalFileDocument.scala
 
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/ReadonlyLocalFileDocument.scala
index 75302656c4..14c9d1688e 100644
--- 
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/ReadonlyLocalFileDocument.scala
+++ 
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/ReadonlyLocalFileDocument.scala
@@ -54,7 +54,11 @@ private[storage] class ReadonlyLocalFileDocument(uri: URI)
   override def get(): Iterator[Nothing] =
     throw new NotImplementedError("get is not supported for 
ReadonlyLocalFileDocument")
 
-  override def getRange(from: Int, until: Int): Iterator[Nothing] =
+  override def getRange(
+      from: Int,
+      until: Int,
+      columns: Option[Seq[String]] = None
+  ): Iterator[Nothing] =
     throw new NotImplementedError("getRange is not supported for 
ReadonlyLocalFileDocument")
 
   override def getAfter(offset: Int): Iterator[Nothing] =
diff --git 
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/ReadonlyVirtualDocument.scala
 
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/ReadonlyVirtualDocument.scala
index 19710e2fc1..47a5511284 100644
--- 
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/ReadonlyVirtualDocument.scala
+++ 
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/ReadonlyVirtualDocument.scala
@@ -52,9 +52,10 @@ trait ReadonlyVirtualDocument[T] {
     * Get an iterator of a sequence starting from index `from`, until index 
`until`.
     * @param from the starting index (inclusive)
     * @param until the ending index (exclusive)
+    * @param columns optional sequence of column names to retrieve. If None, 
retrieves all columns.
     * @return an iterator that returns data items of type T
     */
-  def getRange(from: Int, until: Int): Iterator[T]
+  def getRange(from: Int, until: Int, columns: Option[Seq[String]] = None): 
Iterator[T]
 
   /**
     * Get an iterator of all items after the specified index `offset`.
diff --git 
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/VirtualDocument.scala
 
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/VirtualDocument.scala
index c0407c37d2..8e72c52a80 100644
--- 
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/VirtualDocument.scala
+++ 
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/VirtualDocument.scala
@@ -55,9 +55,10 @@ abstract class VirtualDocument[T] extends 
ReadonlyVirtualDocument[T] {
     * get an iterator of a sequence starting from index `from`, until index 
`until`
     * @param from the starting index (inclusive)
     * @param until the ending index (exclusive)
+    * @param columns the columns to be projected
     * @return an iterator that returns data items of type T
     */
-  def getRange(from: Int, until: Int): Iterator[T] =
+  def getRange(from: Int, until: Int, columns: Option[Seq[String]] = None): 
Iterator[T] =
     throw new NotImplementedError("getRange method is not implemented")
 
   /**
diff --git 
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/result/iceberg/IcebergDocument.scala
 
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/result/iceberg/IcebergDocument.scala
index cef8273afb..e238ab7417 100644
--- 
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/result/iceberg/IcebergDocument.scala
+++ 
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/result/iceberg/IcebergDocument.scala
@@ -105,8 +105,8 @@ private[storage] class IcebergDocument[T >: Null <: AnyRef](
   /**
     * Get records within a specified range [from, until).
     */
-  override def getRange(from: Int, until: Int): Iterator[T] = {
-    getUsingFileSequenceOrder(from, Some(until))
+  override def getRange(from: Int, until: Int, columns: Option[Seq[String]] = 
None): Iterator[T] = {
+    getUsingFileSequenceOrder(from, Some(until), columns)
   }
 
   /**
@@ -150,8 +150,13 @@ private[storage] class IcebergDocument[T >: Null <: 
AnyRef](
     *
     * @param from  start from which record inclusively, if 0 means start from 
the first
     * @param until end at which record exclusively, if None means read to the 
table's EOF
+    * @param columns columns to be projected
     */
-  private def getUsingFileSequenceOrder(from: Int, until: Option[Int]): 
Iterator[T] =
+  private def getUsingFileSequenceOrder(
+      from: Int,
+      until: Option[Int],
+      columns: Option[Seq[String]] = None
+  ): Iterator[T] =
     withReadLock(lock) {
       new Iterator[T] {
         private val iteLock = new ReentrantLock()
@@ -259,9 +264,13 @@ private[storage] class IcebergDocument[T >: Null <: 
AnyRef](
 
           while (!currentRecordIterator.hasNext && usableFileIterator.hasNext) 
{
             val nextFile = usableFileIterator.next()
+            val schemaToUse = columns match {
+              case Some(cols) => tableSchema.select(cols.asJava)
+              case None       => tableSchema
+            }
             currentRecordIterator = IcebergUtil.readDataFileAsIterator(
               nextFile.file(),
-              tableSchema,
+              schemaToUse,
               table.get
             )
 
@@ -281,7 +290,11 @@ private[storage] class IcebergDocument[T >: Null <: 
AnyRef](
 
           val record = currentRecordIterator.next()
           numOfReturnedRecords += 1
-          deserde(tableSchema, record)
+          val schemaToUse = columns match {
+            case Some(cols) => tableSchema.select(cols.asJava)
+            case None       => tableSchema
+          }
+          deserde(schemaToUse, record)
         }
       }
     }
diff --git 
a/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala
 
b/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala
index aa907ec303..30c657746f 100644
--- 
a/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala
+++ 
b/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala
@@ -56,6 +56,7 @@ class ConfigResource {
       ),
       "activeTimeInMinutes" -> 
GuiConfig.guiWorkflowWorkspaceActiveTimeInMinutes,
       "copilotEnabled" -> GuiConfig.guiWorkflowWorkspaceCopilotEnabled,
+      "limitColumns" -> GuiConfig.guiWorkflowWorkspaceLimitColumns,
       // flags from the auth.conf if needed
       "expirationTimeInMinutes" -> AuthConfig.jwtExpirationMinutes
     )
diff --git a/frontend/src/app/common/service/gui-config.service.mock.ts 
b/frontend/src/app/common/service/gui-config.service.mock.ts
index 610169a786..daa8adfd22 100644
--- a/frontend/src/app/common/service/gui-config.service.mock.ts
+++ b/frontend/src/app/common/service/gui-config.service.mock.ts
@@ -49,6 +49,7 @@ export class MockGuiConfigService {
     expirationTimeInMinutes: 2880,
     activeTimeInMinutes: 15,
     copilotEnabled: false,
+    limitColumns: 15,
   };
 
   get env(): GuiConfig {
diff --git a/frontend/src/app/common/type/gui-config.ts 
b/frontend/src/app/common/type/gui-config.ts
index d9b4ad279a..b47dfa0ab1 100644
--- a/frontend/src/app/common/type/gui-config.ts
+++ b/frontend/src/app/common/type/gui-config.ts
@@ -40,6 +40,7 @@ export interface GuiConfig {
   expirationTimeInMinutes: number;
   activeTimeInMinutes: number;
   copilotEnabled: boolean;
+  limitColumns: number;
 }
 
 export interface SidebarTabs {
diff --git 
a/frontend/src/app/workspace/component/result-panel/result-panel.component.ts 
b/frontend/src/app/workspace/component/result-panel/result-panel.component.ts
index 628fd3c7a3..077fac2de3 100644
--- 
a/frontend/src/app/workspace/component/result-panel/result-panel.component.ts
+++ 
b/frontend/src/app/workspace/component/result-panel/result-panel.component.ts
@@ -52,7 +52,7 @@ import { CompilationState } from 
"../../types/workflow-compiling.interface";
 import { WorkflowFatalError } from "../../types/workflow-websocket.interface";
 
 export const DEFAULT_WIDTH = 800;
-export const DEFAULT_HEIGHT = 300;
+export const DEFAULT_HEIGHT = 500;
 /**
  * ResultPanelComponent is the bottom level area that displays the
  *  execution result of a workflow after the execution finishes.
diff --git 
a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.html
 
b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.html
index 2f13bd6984..df0b18f635 100644
--- 
a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.html
+++ 
b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.html
@@ -25,6 +25,40 @@
 <div
   [hidden]="!currentColumns"
   class="result-table">
+  <div
+    class="column-search"
+    style="margin-bottom: 8px; display: flex; justify-content: flex-end">
+    <input
+      nz-input
+      placeholder="Search Columns"
+      (input)="onColumnSearch($event)"
+      style="width: 200px" />
+  </div>
+  <div
+    class="column-navigation"
+    style="margin-bottom: 8px; display: flex; justify-content: flex-end; gap: 
8px"
+    [hidden]="currentColumnOffset === 0 && (!currentColumns || 
currentColumns.length < columnLimit)">
+    <button
+      nz-button
+      nzType="default"
+      (click)="onColumnShiftLeft()"
+      [disabled]="currentColumnOffset === 0">
+      <i
+        nz-icon
+        nzType="left"></i>
+      Previous Columns
+    </button>
+    <button
+      nz-button
+      nzType="default"
+      (click)="onColumnShiftRight()"
+      [disabled]="!currentColumns || currentColumns.length < columnLimit">
+      Next Columns
+      <i
+        nz-icon
+        nzType="right"></i>
+    </button>
+  </div>
   <div class="table-container">
     <nz-table
       #basicTable
diff --git 
a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.spec.ts
 
b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.spec.ts
index 8ef0ad4d9a..8c36110ae3 100644
--- 
a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.spec.ts
+++ 
b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.spec.ts
@@ -25,6 +25,7 @@ import { StubOperatorMetadataService } from 
"../../../service/operator-metadata/
 import { HttpClientTestingModule } from "@angular/common/http/testing";
 import { NzModalModule } from "ng-zorro-antd/modal";
 import { commonTestProviders } from "../../../../common/testing/test-utils";
+import { GuiConfigService } from 
"../../../../common/service/gui-config.service";
 
 describe("ResultTableFrameComponent", () => {
   let component: ResultTableFrameComponent;
@@ -39,6 +40,14 @@ describe("ResultTableFrameComponent", () => {
           provide: OperatorMetadataService,
           useClass: StubOperatorMetadataService,
         },
+        {
+          provide: GuiConfigService,
+          useValue: {
+            env: {
+              limitColumns: 15,
+            },
+          },
+        },
         ...commonTestProviders,
       ],
     }).compileComponents();
@@ -60,4 +69,8 @@ describe("ResultTableFrameComponent", () => {
 
     expect(component.currentResult).toEqual([{ test: "property" }]);
   });
+
+  it("should set columnLimit from config", () => {
+    expect(component.columnLimit).toEqual(15);
+  });
 });
diff --git 
a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.ts
 
b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.ts
index 591c41e53a..abb6daa882 100644
--- 
a/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.ts
+++ 
b/frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.ts
@@ -31,6 +31,7 @@ import { DomSanitizer, SafeHtml } from 
"@angular/platform-browser";
 import { ResultExportationComponent } from 
"../../result-exportation/result-exportation.component";
 import { SchemaAttribute } from "../../../types/workflow-compiling.interface";
 import { WorkflowStatusService } from 
"../../../service/workflow-status/workflow-status.service";
+import { GuiConfigService } from 
"../../../../common/service/gui-config.service";
 
 /**
  * The Component will display the result in an excel table format,
@@ -66,6 +67,9 @@ export class ResultTableFrameComponent implements OnInit, 
OnChanges {
   currentPageIndex: number = 1;
   totalNumTuples: number = 0;
   pageSize = 5;
+  currentColumnOffset = 0;
+  columnLimit = 25;
+  columnSearch = "";
   panelHeight = 0;
   tableStats: Record<string, Record<string, number>> = {};
   prevTableStats: Record<string, Record<string, number>> = {};
@@ -81,7 +85,8 @@ export class ResultTableFrameComponent implements OnInit, 
OnChanges {
     private resizeService: PanelResizeService,
     private changeDetectorRef: ChangeDetectorRef,
     private sanitizer: DomSanitizer,
-    private workflowStatusService: WorkflowStatusService
+    private workflowStatusService: WorkflowStatusService,
+    private guiConfigService: GuiConfigService
   ) {}
 
   ngOnChanges(changes: SimpleChanges): void {
@@ -114,6 +119,8 @@ export class ResultTableFrameComponent implements OnInit, 
OnChanges {
         }
       });
 
+    this.columnLimit = this.guiConfigService.env.limitColumns;
+
     this.workflowResultService
       .getResultUpdateStream()
       .pipe(untilDestroyed(this))
@@ -233,8 +240,37 @@ export class ResultTableFrameComponent implements OnInit, 
OnChanges {
     return this.sanitizer.bypassSecurityTrustHtml(styledValue);
   }
 
+  /**
+   * Adjusts the number of result rows displayed per page based on the
+   * available vertical space of the Texera results panel.
+   *
+   * The method accounts for fixed UI elements within the panel—such as
+   * headers, column navigation controls, pagination, and the search bar—
+   * to determine the remaining space available for rendering result rows.
+   * The page size is then recalculated using the height of a single table row.
+   *
+   * To maintain a stable user experience during panel resizes, the current
+   * page index is recomputed so that the previously visible results remain
+   * in view and the user does not experience an abrupt jump in the dataset.
+   *
+   * @param panelHeight - The total height (in pixels) of the results panel.
+   */
   private adjustPageSizeBasedOnPanelSize(panelHeight: number) {
-    const newPageSize = Math.max(1, Math.floor((panelHeight - 38.62 - 64.27 - 
56.6 - 32.63) / 38.62));
+    const TABLE_HEADER_HEIGHT = 38.62;
+    const PANEL_HEADER_HEIGHT = 64.27; // Includes panel title and tab bar
+    const COLUMN_NAVIGATION_HEIGHT = 56.6; // Previous/Next columns controls
+    const PAGINATION_HEIGHT = 32.63;
+    const SEARCH_BAR_HEIGHT_WITH_MARGIN = 77; // Approximate height for search 
bar and margins
+    const ROW_HEIGHT = 38.62;
+
+    const usedHeight =
+      TABLE_HEADER_HEIGHT +
+      PANEL_HEADER_HEIGHT +
+      COLUMN_NAVIGATION_HEIGHT +
+      PAGINATION_HEIGHT +
+      SEARCH_BAR_HEIGHT_WITH_MARGIN;
+
+    const newPageSize = Math.max(1, Math.floor((panelHeight - usedHeight) / 
ROW_HEIGHT));
 
     const oldOffset = (this.currentPageIndex - 1) * this.pageSize;
 
@@ -329,7 +365,7 @@ export class ResultTableFrameComponent implements OnInit, 
OnChanges {
     }
     this.isLoadingResult = true;
     paginatedResultService
-      .selectPage(this.currentPageIndex, this.pageSize)
+      .selectPage(this.currentPageIndex, this.pageSize, 
this.currentColumnOffset, this.columnLimit, this.columnSearch)
       .pipe(untilDestroyed(this))
       .subscribe(pageData => {
         if (this.currentPageIndex === pageData.pageIndex) {
@@ -405,4 +441,25 @@ export class ResultTableFrameComponent implements OnInit, 
OnChanges {
       nzFooter: null,
     });
   }
+
+  onColumnShiftLeft(): void {
+    if (this.currentColumnOffset > 0) {
+      this.currentColumnOffset = Math.max(0, this.currentColumnOffset - 
this.columnLimit);
+      this.changePaginatedResultData();
+    }
+  }
+
+  onColumnShiftRight(): void {
+    if (this.currentColumns && this.currentColumns.length === 
this.columnLimit) {
+      this.currentColumnOffset += this.columnLimit;
+      this.changePaginatedResultData();
+    }
+  }
+
+  onColumnSearch(event: Event): void {
+    const input = event.target as HTMLInputElement;
+    this.columnSearch = input.value;
+    this.currentColumnOffset = 0;
+    this.changePaginatedResultData();
+  }
 }
diff --git 
a/frontend/src/app/workspace/service/workflow-result/workflow-result.service.ts 
b/frontend/src/app/workspace/service/workflow-result/workflow-result.service.ts
index 330b9b5bad..96937ccbec 100644
--- 
a/frontend/src/app/workspace/service/workflow-result/workflow-result.service.ts
+++ 
b/frontend/src/app/workspace/service/workflow-result/workflow-result.service.ts
@@ -312,11 +312,18 @@ export class OperatorPaginationResultService {
     );
   }
 
-  public selectPage(pageIndex: number, pageSize: number): 
Observable<PaginatedResultEvent> {
+  public selectPage(
+    pageIndex: number,
+    pageSize: number,
+    columnOffset: number = 0,
+    columnLimit: number = Number.MAX_SAFE_INTEGER,
+    columnSearch: string = ""
+  ): Observable<PaginatedResultEvent> {
     // update currently selected page
     this.currentPageIndex = pageIndex;
     // first fetch from frontend result cache
-    const pageCache = this.resultCache.get(pageIndex);
+    const useCache = columnOffset === 0 && columnLimit === 
Number.MAX_SAFE_INTEGER && columnSearch === "";
+    const pageCache = useCache ? this.resultCache.get(pageIndex) : undefined;
     if (pageCache) {
       return of(<PaginatedResultEvent>{
         requestID: "",
@@ -334,6 +341,9 @@ export class OperatorPaginationResultService {
         operatorID,
         pageIndex,
         pageSize,
+        columnOffset,
+        columnLimit,
+        columnSearch,
       });
       const pendingRequestSubject = new Subject<PaginatedResultEvent>();
       this.pendingRequests.set(requestID, pendingRequestSubject);
diff --git a/frontend/src/app/workspace/types/workflow-websocket.interface.ts 
b/frontend/src/app/workspace/types/workflow-websocket.interface.ts
index 15e2a2809d..afd5ea6f04 100644
--- a/frontend/src/app/workspace/types/workflow-websocket.interface.ts
+++ b/frontend/src/app/workspace/types/workflow-websocket.interface.ts
@@ -94,6 +94,9 @@ export type PaginationRequest = Readonly<{
   operatorID: string;
   pageIndex: number;
   pageSize: number;
+  columnOffset?: number;
+  columnLimit?: number;
+  columnSearch?: string;
 }>;
 
 export type PaginatedResultEvent = Readonly<{

Reply via email to