mirror of
https://github.com/xtr-dev/payload-automation.git
synced 2025-12-11 09:13:24 +00:00
## Critical Fixes Implemented: ### 1. Hook Execution Reliability (src/plugin/index.ts) - Replaced fragile global variable pattern with proper dependency injection - Added structured executor registry with initialization tracking - Implemented proper logging using PayloadCMS logger instead of console - Added graceful handling for executor unavailability scenarios ### 2. Error Handling & Workflow Run Tracking - Fixed error swallowing in hook execution - Added createFailedWorkflowRun() to track hook execution failures - Improved error categorization and user-friendly error messages - Enhanced workflow run status tracking with detailed context ### 3. Enhanced HTTP Step (src/steps/) - Complete rewrite of HTTP request handler with enterprise features: - Multiple authentication methods (Bearer, Basic Auth, API Key) - Configurable timeouts and retry logic with exponential backoff - Comprehensive error handling for different failure scenarios - Support for all HTTP methods with proper request/response parsing - Request duration tracking and detailed logging ### 4. User Experience Improvements - Added StatusCell component with visual status indicators - Created ErrorDisplay component with user-friendly error explanations - Added WorkflowExecutionStatus component for real-time execution monitoring - Enhanced collections with better error display and conditional fields ### 5. Comprehensive Testing Suite - Added hook-reliability.spec.ts: Tests executor availability and concurrent execution - Added error-scenarios.spec.ts: Tests timeout, network, validation, and HTTP errors - Added webhook-triggers.spec.ts: Tests webhook endpoints, conditions, and concurrent requests - Fixed existing test to work with enhanced HTTP step schema ## Technical Improvements: - Proper TypeScript interfaces for all new components - Safe serialization handling for circular references - Comprehensive logging with structured data - Modular component architecture with proper exports - Enhanced collection schemas with conditional field visibility ## Impact: - Eliminates silent workflow execution failures - Provides clear error visibility for users - Makes HTTP requests production-ready with auth and retry capabilities - Significantly improves debugging and monitoring experience - Adds comprehensive test coverage for reliability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
231 lines
6.9 KiB
TypeScript
231 lines
6.9 KiB
TypeScript
'use client'
|
||
|
||
import React, { useState, useEffect } from 'react'
|
||
import { Button } from '@payloadcms/ui'
|
||
|
||
interface WorkflowRun {
|
||
id: string
|
||
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
|
||
startedAt: string
|
||
completedAt?: string
|
||
error?: string
|
||
triggeredBy: string
|
||
}
|
||
|
||
interface WorkflowExecutionStatusProps {
|
||
workflowId: string | number
|
||
}
|
||
|
||
export const WorkflowExecutionStatus: React.FC<WorkflowExecutionStatusProps> = ({ workflowId }) => {
|
||
const [runs, setRuns] = useState<WorkflowRun[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [expanded, setExpanded] = useState(false)
|
||
|
||
useEffect(() => {
|
||
const fetchRecentRuns = async () => {
|
||
try {
|
||
const response = await fetch(`/api/workflow-runs?where[workflow][equals]=${workflowId}&limit=5&sort=-startedAt`)
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setRuns(data.docs || [])
|
||
}
|
||
} catch (error) {
|
||
console.warn('Failed to fetch workflow runs:', error)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
fetchRecentRuns()
|
||
}, [workflowId])
|
||
|
||
if (loading) {
|
||
return (
|
||
<div style={{ padding: '16px', color: '#6B7280' }}>
|
||
Loading execution history...
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (runs.length === 0) {
|
||
return (
|
||
<div style={{
|
||
padding: '16px',
|
||
backgroundColor: '#F9FAFB',
|
||
border: '1px solid #E5E7EB',
|
||
borderRadius: '8px',
|
||
color: '#6B7280',
|
||
textAlign: 'center'
|
||
}}>
|
||
📋 No execution history yet
|
||
<br />
|
||
<small>This workflow hasn't been triggered yet.</small>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const getStatusIcon = (status: string) => {
|
||
switch (status) {
|
||
case 'pending': return '⏳'
|
||
case 'running': return '🔄'
|
||
case 'completed': return '✅'
|
||
case 'failed': return '❌'
|
||
case 'cancelled': return '⏹️'
|
||
default: return '❓'
|
||
}
|
||
}
|
||
|
||
const getStatusColor = (status: string) => {
|
||
switch (status) {
|
||
case 'pending': return '#6B7280'
|
||
case 'running': return '#3B82F6'
|
||
case 'completed': return '#10B981'
|
||
case 'failed': return '#EF4444'
|
||
case 'cancelled': return '#F59E0B'
|
||
default: return '#6B7280'
|
||
}
|
||
}
|
||
|
||
const formatDate = (dateString: string) => {
|
||
const date = new Date(dateString)
|
||
const now = new Date()
|
||
const diffMs = now.getTime() - date.getTime()
|
||
|
||
if (diffMs < 60000) { // Less than 1 minute
|
||
return 'Just now'
|
||
} else if (diffMs < 3600000) { // Less than 1 hour
|
||
return `${Math.floor(diffMs / 60000)} min ago`
|
||
} else if (diffMs < 86400000) { // Less than 1 day
|
||
return `${Math.floor(diffMs / 3600000)} hrs ago`
|
||
} else {
|
||
return date.toLocaleDateString()
|
||
}
|
||
}
|
||
|
||
const getDuration = (startedAt: string, completedAt?: string) => {
|
||
const start = new Date(startedAt)
|
||
const end = completedAt ? new Date(completedAt) : new Date()
|
||
const diffMs = end.getTime() - start.getTime()
|
||
|
||
if (diffMs < 1000) return '<1s'
|
||
if (diffMs < 60000) return `${Math.floor(diffMs / 1000)}s`
|
||
if (diffMs < 3600000) return `${Math.floor(diffMs / 60000)}m ${Math.floor((diffMs % 60000) / 1000)}s`
|
||
return `${Math.floor(diffMs / 3600000)}h ${Math.floor((diffMs % 3600000) / 60000)}m`
|
||
}
|
||
|
||
const recentRun = runs[0]
|
||
const recentStatus = getStatusIcon(recentRun.status)
|
||
const recentColor = getStatusColor(recentRun.status)
|
||
|
||
return (
|
||
<div style={{
|
||
border: '1px solid #E5E7EB',
|
||
borderRadius: '8px',
|
||
backgroundColor: '#FAFAFA'
|
||
}}>
|
||
{/* Summary Header */}
|
||
<div style={{
|
||
padding: '16px',
|
||
borderBottom: expanded ? '1px solid #E5E7EB' : 'none',
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center'
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||
<span style={{ fontSize: '20px' }}>{recentStatus}</span>
|
||
<div>
|
||
<div style={{ fontWeight: '600', color: recentColor }}>
|
||
Last run: {recentRun.status}
|
||
</div>
|
||
<div style={{ fontSize: '13px', color: '#6B7280' }}>
|
||
{formatDate(recentRun.startedAt)} • Duration: {getDuration(recentRun.startedAt, recentRun.completedAt)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<Button
|
||
onClick={() => setExpanded(!expanded)}
|
||
size="small"
|
||
buttonStyle="secondary"
|
||
>
|
||
{expanded ? 'Hide' : 'Show'} History ({runs.length})
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Detailed History */}
|
||
{expanded && (
|
||
<div style={{ padding: '16px' }}>
|
||
<h4 style={{ margin: '0 0 12px 0', fontSize: '14px', fontWeight: '600' }}>
|
||
Recent Executions
|
||
</h4>
|
||
|
||
{runs.map((run, index) => (
|
||
<div
|
||
key={run.id}
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
padding: '8px 12px',
|
||
marginBottom: index < runs.length - 1 ? '8px' : '0',
|
||
backgroundColor: 'white',
|
||
border: '1px solid #E5E7EB',
|
||
borderRadius: '6px'
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||
<span style={{ fontSize: '16px' }}>
|
||
{getStatusIcon(run.status)}
|
||
</span>
|
||
|
||
<div>
|
||
<div style={{
|
||
fontSize: '13px',
|
||
fontWeight: '500',
|
||
color: getStatusColor(run.status)
|
||
}}>
|
||
{run.status.charAt(0).toUpperCase() + run.status.slice(1)}
|
||
</div>
|
||
<div style={{ fontSize: '12px', color: '#6B7280' }}>
|
||
{formatDate(run.startedAt)} • {run.triggeredBy}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{
|
||
fontSize: '12px',
|
||
color: '#6B7280',
|
||
textAlign: 'right'
|
||
}}>
|
||
<div>
|
||
{getDuration(run.startedAt, run.completedAt)}
|
||
</div>
|
||
{run.error && (
|
||
<div style={{ color: '#EF4444', marginTop: '2px' }}>
|
||
Error
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
|
||
<div style={{
|
||
marginTop: '12px',
|
||
textAlign: 'center'
|
||
}}>
|
||
<Button
|
||
onClick={() => {
|
||
// Navigate to workflow runs filtered by this workflow
|
||
window.location.href = `/admin/collections/workflow-runs?where[workflow][equals]=${workflowId}`
|
||
}}
|
||
size="small"
|
||
buttonStyle="secondary"
|
||
>
|
||
View All Runs →
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
} |