Преглед изворни кода

feat(webui): make executions inspectable from the list

The executions page showed five columns, none of them actionable, and its
Trigger column was always blank. The blank column and the missing errors had
the same cause: transformExecutionListItem mapped six fields and dropped
triggerType and error, both of which the API already returns.

Rows now open the execution. Clicking one navigates to the workflow editor
with ?execution=<id>, which loads it into the existing read-only viewer, where
each node shows the input and output it actually had. That viewer already
existed for the in-editor execution list; a standalone execution page would
have meant a second, weaker version of it.

A failed execution now prints its error beneath the row, in full. The one that
prompted this reads "Document with ID '8293794f...' already exists", which was
invisible in the UI and is a real bug in its own right.
fszontagh пре 1 месец
родитељ
комит
553d9b5a6a

+ 4 - 0
webui/src/api/workflows.ts

@@ -287,6 +287,8 @@ export interface ExecutionListItem {
   workflowId: string
   workflowName: string
   status: string
+  triggerType: string
+  error: string
   startedAt: number
   finishedAt: number
 }
@@ -298,6 +300,8 @@ function transformExecutionListItem(data: any): ExecutionListItem {
     workflowId: data.workflowId,
     workflowName: data.workflowName,
     status: data.status,
+    triggerType: data.triggerType || '',
+    error: data.error || '',
     startedAt: data.startedAt,
     finishedAt: data.finishedAt,
   }

+ 34 - 3
webui/src/pages/ExecutionsPage.tsx

@@ -1,9 +1,10 @@
-import { useState } from 'react'
+import { useState, Fragment } from 'react'
+import { useNavigate } from 'react-router-dom'
 import { useQuery } from '@tanstack/react-query'
 import { executionsApi } from '../api/workflows'
 import { useExecutionListUpdates } from '../hooks/useExecutionListUpdates'
 import { formatDistanceToNow } from 'date-fns'
-import { CheckCircle, XCircle, Clock, Ban, RefreshCw, ChevronLeft, ChevronRight } from 'lucide-react'
+import { CheckCircle, XCircle, Clock, Ban, RefreshCw, ChevronLeft, ChevronRight, AlertCircle } from 'lucide-react'
 import clsx from 'clsx'
 
 // Helper to safely format timestamps that might be invalid or too large
@@ -21,8 +22,19 @@ function safeFormatDistanceToNow(timestamp: number): string {
 const PAGE_SIZE = 20
 
 export default function ExecutionsPage() {
+  const navigate = useNavigate()
   const [page, setPage] = useState(1)
 
+  // The workflow editor already renders an execution across the canvas with each
+  // node's input and output, so open it there rather than building a second
+  // viewer that would show less.
+  const openExecution = (execution: { id: string; workflowId: string }) => {
+    if (!execution.workflowId) {
+      return
+    }
+    navigate(`/workflows/${execution.workflowId}?execution=${encodeURIComponent(execution.id)}`)
+  }
+
   const { data, isLoading, refetch } = useQuery({
     queryKey: ['executions', page],
     queryFn: () => executionsApi.list(undefined, undefined, page, PAGE_SIZE),
@@ -112,7 +124,12 @@ export default function ExecutionsPage() {
               </thead>
               <tbody className="divide-y divide-gray-200 dark:divide-slate-700">
                 {executions.map((execution: any) => (
-                  <tr key={execution.id} className="hover:bg-gray-50 dark:hover:bg-slate-700/50">
+                  <Fragment key={execution.id}>
+                  <tr
+                    onClick={() => openExecution(execution)}
+                    title="Open this execution in the workflow editor"
+                    className="hover:bg-gray-50 dark:hover:bg-slate-700/50 cursor-pointer"
+                  >
                     <td className="px-4 py-3">
                       <div className="flex items-center gap-2">
                         {getStatusIcon(execution.status)}
@@ -145,6 +162,20 @@ export default function ExecutionsPage() {
                         : '-'}
                     </td>
                   </tr>
+                  {execution.error && (
+                    <tr
+                      onClick={() => openExecution(execution)}
+                      className="cursor-pointer hover:bg-gray-50 dark:hover:bg-slate-700/50"
+                    >
+                      <td colSpan={5} className="px-4 pb-3 pt-0">
+                        <div className="flex items-start gap-1.5 text-red-600 dark:text-red-400 text-xs">
+                          <AlertCircle className="w-3.5 h-3.5 shrink-0 mt-0.5" />
+                          <span className="break-all">{execution.error}</span>
+                        </div>
+                      </td>
+                    </tr>
+                  )}
+                  </Fragment>
                 ))}
               </tbody>
             </table>

+ 20 - 1
webui/src/pages/WorkflowEditorPage.tsx

@@ -1,7 +1,7 @@
 import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
 import dagre from 'dagre'
-import { workflowsApi, nodesApi, Workflow, NodeDefinition, NodeOutput } from '../api/workflows'
+import { workflowsApi, nodesApi, executionsApi, Workflow, NodeDefinition, NodeOutput } from '../api/workflows'
 import ReactFlow, {
   Background,
   Controls,
@@ -386,6 +386,25 @@ function WorkflowEditorInner() {
     }
   }, [workflow])
 
+  // Open a specific execution when arrived at via ?execution=<id>, which is how
+  // the executions list links here. Waits for the canvas to have nodes, since
+  // the viewer maps results onto them, and runs once per execution id.
+  const openedExecutionRef = useRef<string | null>(null)
+  useEffect(() => {
+    const executionId = searchParams.get('execution')
+    if (!executionId || nodes.length === 0 || openedExecutionRef.current === executionId) {
+      return
+    }
+    openedExecutionRef.current = executionId
+
+    executionsApi
+      .getDetail(executionId)
+      .then((execution) => handleViewExecution(execution))
+      .catch((error: any) => {
+        showToast('error', `Could not open execution: ${error.message}`)
+      })
+  }, [searchParams, nodes.length, handleViewExecution, showToast])
+
   // Fetch all available collections from the database
   const { data: allCollectionsData } = useQuery({
     queryKey: ['database-collections'],