WorkflowLogsCard.tsx112 lines · main
1import { motion } from 'framer-motion'
2import { CircleDotDashed, GitMerge, X } from 'lucide-react'
3import { useEffect, useRef } from 'react'
4import { Button, Card, CardContent, CardHeader, CardTitle } from 'ui'
5
6import { ActionRun } from '@/data/actions/action-detail-query'
7
8interface WorkflowLogsCardProps {
9 workflowRun: ActionRun | null | undefined
10 logs: string | undefined
11 isLoading?: boolean
12 onClose?: () => void
13 // Override props for failed workflows
14 overrideTitle?: string
15 overrideDescription?: string
16 overrideIcon?: React.ReactNode
17 overrideAction?: React.ReactNode
18}
19
20export const WorkflowLogsCard = ({
21 workflowRun,
22 logs,
23 isLoading = false,
24 onClose,
25 overrideTitle,
26 overrideDescription,
27 overrideIcon,
28 overrideAction,
29}: WorkflowLogsCardProps) => {
30 const scrollRef = useRef<HTMLDivElement>(null)
31
32 // Auto-scroll to bottom when logs change
33 useEffect(() => {
34 if (scrollRef.current && logs) {
35 scrollRef.current.scrollTop = scrollRef.current.scrollHeight
36 }
37 }, [logs])
38
39 const workflowRunStatus = workflowRun?.status
40 const isSuccess = workflowRunStatus === 'SUCCESS'
41 const isFailed = workflowRunStatus === 'FAILED'
42 const isPolling = workflowRunStatus === 'RUNNING'
43
44 const displayTitle =
45 overrideTitle ||
46 (isPolling
47 ? 'Processing...'
48 : isSuccess
49 ? 'Workflow completed successfully'
50 : isFailed
51 ? 'Workflow failed'
52 : 'Workflow completed')
53
54 const displayIcon =
55 overrideIcon ||
56 (isPolling ? (
57 <motion.div
58 animate={{ rotate: 360 }}
59 transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
60 >
61 <CircleDotDashed size={16} strokeWidth={1.5} className="text-warning" />
62 </motion.div>
63 ) : isSuccess ? (
64 <GitMerge size={16} strokeWidth={1.5} className="text-brand" />
65 ) : null)
66
67 return (
68 <Card className="bg-background overflow-hidden h-64 flex flex-col">
69 <CardHeader className={isSuccess ? 'text-brand' : isFailed ? 'text-destructive' : ''}>
70 <div className="flex items-center justify-between">
71 <div className="flex items-center gap-4">
72 {displayIcon}
73 <div>
74 <CardTitle className="text-sm font-medium">{displayTitle}</CardTitle>
75 {overrideDescription && (
76 <div className="text-sm text-foreground-light font-normal mt-0">
77 {overrideDescription}
78 </div>
79 )}
80 </div>
81 </div>
82 <div className="flex items-center gap-4">
83 {overrideAction}
84 {onClose && (
85 <Button
86 type="text"
87 size="tiny"
88 icon={<X size={12} strokeWidth={1.5} />}
89 onClick={onClose}
90 className="h-5 w-5 p-0"
91 />
92 )}
93 </div>
94 </div>
95 </CardHeader>
96 <CardContent
97 ref={scrollRef}
98 className="overflow-hidden border-0 overflow-y-auto relative p-0"
99 >
100 {/* sticky gradient overlay */}
101 <div className="sticky top-0 -mb-8 h-8 bg-linear-to-b from-background to-transparent pointer-events-none z-10" />
102 {logs ? (
103 <pre className="p-6 text-xs text-foreground-light p-0 rounded-sm">{logs}</pre>
104 ) : (
105 <pre className="p-6 text-sm text-foreground-light rounded-sm">
106 {isLoading || isPolling ? 'Initializing workflow...' : 'Waiting for logs...'}
107 </pre>
108 )}
109 </CardContent>
110 </Card>
111 )
112}