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 704219fc2d3..3d78eef3035 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 8c4ecda946b..fb6a8c928a4 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 cfa653316d2..f39453f651b 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 74a68ee65ea..a4a588e0f04 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 0d67e85d56d..d3a2aadd934 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 +
  • + +