WorkflowLogsCard.tsx 3.5 KB

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