33import { Button } from '@comp/ui/button' ;
44import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from '@comp/ui/select' ;
55import { useRealtimeRun } from '@trigger.dev/react-hooks' ;
6- import { CheckCircle2 , Info , Loader2 , RefreshCw , X } from 'lucide-react' ;
6+ import { AlertTriangle , CheckCircle2 , Info , Loader2 , RefreshCw , X } from 'lucide-react' ;
77import { useEffect , useState } from 'react' ;
8+ import type { IntegrationRunOutput } from '../types' ;
89import { FindingsTable } from './FindingsTable' ;
910
1011interface Finding {
@@ -23,6 +24,7 @@ interface ResultsViewProps {
2324 scanAccessToken : string | null ;
2425 onRunScan : ( ) => Promise < string | null > ;
2526 isScanning : boolean ;
27+ runOutput : IntegrationRunOutput | null ;
2628}
2729
2830const severityOrder = { critical : 0 , high : 1 , medium : 2 , low : 3 , info : 4 } ;
@@ -50,6 +52,7 @@ export function ResultsView({
5052 scanAccessToken,
5153 onRunScan,
5254 isScanning,
55+ runOutput,
5356} : ResultsViewProps ) {
5457 // Track scan status with Trigger.dev hooks
5558 const { run } = useRealtimeRun ( scanTaskId || '' , {
@@ -64,17 +67,28 @@ export function ResultsView({
6467 const [ selectedSeverity , setSelectedSeverity ] = useState < string > ( 'all' ) ;
6568 const [ showSuccessBanner , setShowSuccessBanner ] = useState ( false ) ;
6669 const [ showErrorBanner , setShowErrorBanner ] = useState ( false ) ;
70+ const [ showOutputErrorBanner , setShowOutputErrorBanner ] = useState ( false ) ;
6771
68- // Show success banner when scan completes, auto-hide after 5 seconds
72+ const runOutputError = runOutput && ! runOutput . success ? runOutput : null ;
73+ const scanSucceeded = scanCompleted && ! runOutputError ;
74+ const outputErrorMessages =
75+ runOutputError ?. errors && runOutputError . errors . length > 0
76+ ? runOutputError . errors
77+ : runOutputError ?. failedIntegrations ?. map (
78+ ( integration ) => `${ integration . name } : ${ integration . error } ` ,
79+ ) ?? [ ] ;
80+
81+ // Show success banner when scan completes successfully, auto-hide after 5 seconds
6982 useEffect ( ( ) => {
70- if ( scanCompleted ) {
83+ if ( scanSucceeded ) {
7184 setShowSuccessBanner ( true ) ;
7285 const timer = setTimeout ( ( ) => {
7386 setShowSuccessBanner ( false ) ;
7487 } , 5000 ) ;
7588 return ( ) => clearTimeout ( timer ) ;
7689 }
77- } , [ scanCompleted ] ) ;
90+ setShowSuccessBanner ( false ) ;
91+ } , [ scanSucceeded ] ) ;
7892
7993 // Auto-dismiss error banner after 30 seconds
8094 useEffect ( ( ) => {
@@ -87,6 +101,16 @@ export function ResultsView({
87101 }
88102 } , [ scanFailed ] ) ;
89103
104+ // Show output error banner when run completes with errors
105+ useEffect ( ( ) => {
106+ if ( runOutputError ) {
107+ setShowOutputErrorBanner ( true ) ;
108+ setShowSuccessBanner ( false ) ;
109+ } else {
110+ setShowOutputErrorBanner ( false ) ;
111+ }
112+ } , [ runOutputError ] ) ;
113+
90114 // Get unique statuses and severities
91115 const uniqueStatuses = Array . from (
92116 new Set ( findings . map ( ( f ) => f . status ) . filter ( Boolean ) as string [ ] ) ,
@@ -146,8 +170,40 @@ export function ResultsView({
146170 </ div >
147171 ) }
148172
173+ { /* Output error banner when run reports errors but job didn't crash */ }
174+ { showOutputErrorBanner && runOutputError && ! isScanning && (
175+ < div className = "bg-destructive/10 flex items-start gap-3 rounded-lg border border-destructive/20 p-4" >
176+ < AlertTriangle className = "text-destructive h-5 w-5 flex-shrink-0" />
177+ < div className = "flex-1 space-y-1" >
178+ < p className = "text-destructive text-sm font-medium" > Scan completed with errors</ p >
179+ < ul className = "text-muted-foreground text-xs leading-relaxed" >
180+ { outputErrorMessages . slice ( 0 , 5 ) . map ( ( message , index ) => (
181+ < li key = { `${ message } -${ index } ` } > • { message } </ li >
182+ ) ) }
183+ { outputErrorMessages . length === 0 && (
184+ < li > Encountered an unknown error while processing integration results.</ li >
185+ ) }
186+ </ ul >
187+ { runOutputError . failedIntegrations && runOutputError . failedIntegrations . length > 0 && (
188+ < p className = "text-muted-foreground text-xs" >
189+ { runOutputError . failedIntegrations . length } integration
190+ { runOutputError . failedIntegrations . length === 1 ? '' : 's' } returned errors.
191+ </ p >
192+ ) }
193+ </ div >
194+ < Button
195+ variant = "ghost"
196+ size = "sm"
197+ onClick = { ( ) => setShowOutputErrorBanner ( false ) }
198+ className = "text-muted-foreground hover:text-foreground h-auto p-1"
199+ >
200+ < X className = "h-4 w-4" />
201+ </ Button >
202+ </ div >
203+ ) }
204+
149205 { /* Propagation delay info banner - only when scan succeeds but returns empty output */ }
150- { scanCompleted && findings . length === 0 && ! isScanning && ! scanFailed && (
206+ { scanCompleted && findings . length === 0 && ! isScanning && ! scanFailed && ! runOutputError && (
151207 < div className = "bg-blue-50 dark:bg-blue-950/20 flex items-center gap-3 rounded-lg border border-blue-200 dark:border-blue-900 p-4" >
152208 < Info className = "text-blue-600 dark:text-blue-400 h-5 w-5 flex-shrink-0" />
153209 < div className = "flex-1" >
0 commit comments