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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-5890-1073b22f8f69d0e44b643047b856efc5f758d665
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 13132a0ba6de90adfbdc18f109221063eb0b11ef
Author: roshiiiiz <[email protected]>
AuthorDate: Tue Jun 30 23:35:58 2026 +0500

    feat(frontend): add sort by execution time with nulls last (#5890)
    
    ### What changes were proposed in this PR?
    This PR adds the ability to sort workflows by "Execution Time" on the
    main Dashboard, and ensures that workflows which have never been
    executed (where execution time is `null`) are pushed to the bottom of
    the list rather than awkwardly floating at the top.
    
    Changes made:
    - Added "Execution time" as an option in the frontend sort dropdown menu
    (`sort-method.ts`, `sort-button.component`).
    - Updated `UnifiedResourceSchema.scala` in the backend to include
    `resourceExecutionTimeField`.
    - Updated `DashboardResource.scala` and
    `WorkflowSearchQueryBuilder.scala` to handle the new sort condition,
    specifically appending `.nullsLast()` so that un-executed workflows drop
    to the bottom of the list.
    
    ### Any related issues, documentation, discussions?
    Closes #3406
    
    ### How was this PR tested?
    Tested manually in a local development environment:
    1. Booted the local dashboard and backend via `sbt`.
    2. Created a new, unexecuted "Untitled Workflow".
    3. Navigated to the Dashboard and sorted by "Execution Time".
    4. Verified that the unexecuted workflow correctly fell to the bottom of
    the list instead of appearing at the top.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    Generated-by: Antigravity (DeepMind)
    
    <img width="1918" height="944" alt="pull 5890"
    
src="https://github.com/user-attachments/assets/defca5ec-1bd0-4810-ae62-ada3a016821b";
    />
    
    ---------
    
    Co-authored-by: Xuan Gu <[email protected]>
---
 .../web/resource/dashboard/DashboardResource.scala | 13 ++++++-----
 .../resource/dashboard/UnifiedResourceSchema.scala |  4 ++++
 .../dashboard/WorkflowSearchQueryBuilder.scala     |  8 +++++++
 .../dashboard/file/WorkflowResourceSpec.scala      | 26 ++++++++++++++++++++++
 .../user/sort-button/sort-button.component.html    |  7 ++++++
 .../user/sort-button/sort-button.component.spec.ts |  7 ++++++
 .../user/sort-button/sort-button.component.ts      |  5 +++++
 frontend/src/app/dashboard/type/sort-method.ts     |  1 +
 8 files changed, 65 insertions(+), 6 deletions(-)

diff --git 
a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DashboardResource.scala
 
b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DashboardResource.scala
index 704219fc2d..3d78eef303 100644
--- 
a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DashboardResource.scala
+++ 
b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DashboardResource.scala
@@ -145,7 +145,7 @@ object DashboardResource {
       searchQueryParams: SearchQueryParams
   ): List[OrderField[_]] = {
     // Regex pattern to extract column name and order direction
-    val pattern = "(Name|CreateTime|EditTime)(Asc|Desc)".r
+    val pattern = "(Name|CreateTime|EditTime|ExecutionTime)(Asc|Desc)".r
 
     searchQueryParams.orderBy match {
       case pattern(column, order) =>
@@ -154,7 +154,7 @@ object DashboardResource {
           case Some(value) =>
             List(order match {
               case "Asc"  => value.asc()
-              case "Desc" => value.desc()
+              case "Desc" => value.desc().nullsLast()
             })
           case None => List()
         }
@@ -165,10 +165,11 @@ object DashboardResource {
   // Helper method to map column names to actual database fields based on 
resource type
   private def getColumnField(columnName: String): Option[Field[_]] = {
     Option(columnName match {
-      case "Name"       => UnifiedResourceSchema.resourceNameField
-      case "CreateTime" => UnifiedResourceSchema.resourceCreationTimeField
-      case "EditTime"   => UnifiedResourceSchema.resourceLastModifiedTimeField
-      case _            => null // Default case for unmatched resource types 
or column names
+      case "Name"          => UnifiedResourceSchema.resourceNameField
+      case "CreateTime"    => UnifiedResourceSchema.resourceCreationTimeField
+      case "EditTime"      => 
UnifiedResourceSchema.resourceLastModifiedTimeField
+      case "ExecutionTime" => UnifiedResourceSchema.resourceExecutionTimeField
+      case _               => null // Default case for unmatched resource 
types or column names
     })
   }
 
diff --git 
a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala
 
b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala
index 8c4ecda946..fb6a8c928a 100644
--- 
a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala
+++ 
b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala
@@ -37,6 +37,7 @@ object UnifiedResourceSchema {
   private val resourceCreationTimeAlias = "resourceCreationTime"
   private val resourceOwnerIdAlias = "resourceOwnerId"
   private val resourceLastModifiedTimeAlias = "resourceLastModifiedTime"
+  private val resourceExecutionTimeAlias = "resourceExecutionTime"
 
   // Use the alias variables to create fields
   val resourceTypeField: Field[_] = DSL.field(DSL.name(resourceTypeAlias))
@@ -45,6 +46,7 @@ object UnifiedResourceSchema {
   val resourceCreationTimeField: Field[_] = 
DSL.field(DSL.name(resourceCreationTimeAlias))
   val resourceOwnerIdField: Field[_] = 
DSL.field(DSL.name(resourceOwnerIdAlias))
   val resourceLastModifiedTimeField: Field[_] = 
DSL.field(DSL.name(resourceLastModifiedTimeAlias))
+  val resourceExecutionTimeField: Field[_] = 
DSL.field(DSL.name(resourceExecutionTimeAlias))
 
   def context =
     SqlServer
@@ -57,6 +59,7 @@ object UnifiedResourceSchema {
       description: Field[String] = DSL.inline(""),
       creationTime: Field[Timestamp] = DSL.cast(null, classOf[Timestamp]),
       lastModifiedTime: Field[Timestamp] = DSL.cast(null, classOf[Timestamp]),
+      executionTime: Field[Timestamp] = DSL.cast(null, classOf[Timestamp]),
       ownerId: Field[Integer] = DSL.cast(null, classOf[Integer]),
       wid: Field[Integer] = DSL.cast(null, classOf[Integer]),
       workflowUserAccess: Field[PrivilegeEnum] = 
DSL.castNull(classOf[PrivilegeEnum]),
@@ -82,6 +85,7 @@ object UnifiedResourceSchema {
         description -> description.as(resourceDescriptionAlias),
         creationTime -> creationTime.as(resourceCreationTimeAlias),
         lastModifiedTime -> lastModifiedTime.as(resourceLastModifiedTimeAlias),
+        executionTime -> executionTime.as(resourceExecutionTimeAlias),
         ownerId -> ownerId.as(resourceOwnerIdAlias),
         wid -> wid.as("wid"),
         workflowUserAccess -> workflowUserAccess.as("workflow_privilege"),
diff --git 
a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilder.scala
 
b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilder.scala
index cfa653316d..f39453f651 100644
--- 
a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilder.scala
+++ 
b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilder.scala
@@ -40,6 +40,14 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder 
{
       creationTime = WORKFLOW.CREATION_TIME,
       wid = WORKFLOW.WID,
       lastModifiedTime = WORKFLOW.LAST_MODIFIED_TIME,
+      executionTime = DSL.field(
+        DSL
+          .select(DSL.max(WORKFLOW_EXECUTIONS.STARTING_TIME))
+          .from(WORKFLOW_EXECUTIONS)
+          .join(WORKFLOW_VERSION)
+          .on(WORKFLOW_EXECUTIONS.VID.eq(WORKFLOW_VERSION.VID))
+          .where(WORKFLOW_VERSION.WID.eq(WORKFLOW.WID))
+      ),
       workflowUserAccess = WORKFLOW_USER_ACCESS.PRIVILEGE,
       uid = WORKFLOW_OF_USER.UID,
       ownerId = WORKFLOW_OF_USER.UID,
diff --git 
a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/file/WorkflowResourceSpec.scala
 
b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/file/WorkflowResourceSpec.scala
index 74a68ee65e..385ad6bcf2 100644
--- 
a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/file/WorkflowResourceSpec.scala
+++ 
b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/file/WorkflowResourceSpec.scala
@@ -780,4 +780,30 @@ class WorkflowResourceSpec
     assert(resources.results(2).workflow.get.workflow.getName == 
"test_workflow1")
   }
 
+  it should "order workflow by execution time correctly" in {
+    // Create several resources with different names (no execution times are 
set, but the SQL query should parse correctly)
+    workflowResource.persistWorkflow(testWorkflow1, sessionUser1)
+    workflowResource.persistWorkflow(testWorkflow3, sessionUser1)
+    workflowResource.persistWorkflow(testWorkflow2, sessionUser1)
+
+    // Retrieve resources ordered by execution time ascending
+    var resources =
+      dashboardResource.searchAllResourcesCall(
+        sessionUser1,
+        SearchQueryParams(resourceType = "workflow", orderBy = 
"ExecutionTimeAsc")
+      )
+
+    // Execution times are null so order is not guaranteed, but we verify it 
returns 3 results
+    assert(resources.results.length == 3)
+
+    // Retrieve resources ordered by execution time descending
+    resources = dashboardResource.searchAllResourcesCall(
+      sessionUser1,
+      SearchQueryParams(resourceType = "workflow", orderBy = 
"ExecutionTimeDesc")
+    )
+
+    // Verify it returns 3 results
+    assert(resources.results.length == 3)
+  }
+
 }
diff --git 
a/frontend/src/app/dashboard/component/user/sort-button/sort-button.component.html
 
b/frontend/src/app/dashboard/component/user/sort-button/sort-button.component.html
index 0d67e85d56..d3a2aadd93 100644
--- 
a/frontend/src/app/dashboard/component/user/sort-button/sort-button.component.html
+++ 
b/frontend/src/app/dashboard/component/user/sort-button/sort-button.component.html
@@ -48,6 +48,13 @@
         By Create Time
       </button>
     </li>
+    <li nz-menu-item>
+      <button
+        (click)="execSort()"
+        nz-button>
+        By Execution Time
+      </button>
+    </li>
     <li nz-menu-item>
       <button
         (click)="ascSort()"
diff --git 
a/frontend/src/app/dashboard/component/user/sort-button/sort-button.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/sort-button/sort-button.component.spec.ts
index f007982cfd..516309e2a0 100644
--- 
a/frontend/src/app/dashboard/component/user/sort-button/sort-button.component.spec.ts
+++ 
b/frontend/src/app/dashboard/component/user/sort-button/sort-button.component.spec.ts
@@ -69,4 +69,11 @@ describe("SortButtonComponent", () => {
     expect(component.sortMethod).toBe(SortMethod.NameDesc);
     expect(emitSpy).toHaveBeenCalledWith(SortMethod.NameDesc);
   });
+
+  it("should handle execSort() correctly", () => {
+    const emitSpy = vi.spyOn(component.sortMethodChange, "emit");
+    component.execSort();
+    expect(component.sortMethod).toBe(SortMethod.ExecutionTimeDesc);
+    expect(emitSpy).toHaveBeenCalledWith(SortMethod.ExecutionTimeDesc);
+  });
 });
diff --git 
a/frontend/src/app/dashboard/component/user/sort-button/sort-button.component.ts
 
b/frontend/src/app/dashboard/component/user/sort-button/sort-button.component.ts
index 8402e55b05..3282784d07 100644
--- 
a/frontend/src/app/dashboard/component/user/sort-button/sort-button.component.ts
+++ 
b/frontend/src/app/dashboard/component/user/sort-button/sort-button.component.ts
@@ -68,4 +68,9 @@ export class SortButtonComponent {
     this.sortMethod = SortMethod.NameDesc;
     this.sortMethodChange.emit(this.sortMethod);
   }
+
+  public execSort(): void {
+    this.sortMethod = SortMethod.ExecutionTimeDesc;
+    this.sortMethodChange.emit(this.sortMethod);
+  }
 }
diff --git a/frontend/src/app/dashboard/type/sort-method.ts 
b/frontend/src/app/dashboard/type/sort-method.ts
index 70ecb5565d..07d506fe6d 100644
--- a/frontend/src/app/dashboard/type/sort-method.ts
+++ b/frontend/src/app/dashboard/type/sort-method.ts
@@ -22,4 +22,5 @@ export enum SortMethod {
   NameDesc,
   CreateTimeDesc,
   EditTimeDesc,
+  ExecutionTimeDesc,
 }

Reply via email to