22
33import { Button } from '@comp/ui/button' ;
44import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from '@comp/ui/select' ;
5- import { useRealtimeRun } from '@trigger.dev/react-hooks ' ;
5+ import type { AnyRealtimeRun } from '@trigger.dev/sdk ' ;
66import { AlertTriangle , CheckCircle2 , Info , Loader2 , RefreshCw , X } from 'lucide-react' ;
7- import { useEffect , useState } from 'react' ;
8- import { isFailureRunStatus , isSuccessfulRunStatus , isTerminalRunStatus } from '../status' ;
9- import type { IntegrationRunOutput } from '../types' ;
7+ import { useMemo , useState } from 'react' ;
108import { FindingsTable } from './FindingsTable' ;
119
1210interface Finding {
@@ -21,127 +19,68 @@ interface Finding {
2119
2220interface ResultsViewProps {
2321 findings : Finding [ ] ;
24- scanTaskId : string | null ;
25- scanAccessToken : string | null ;
2622 onRunScan : ( ) => Promise < string | null > ;
2723 isScanning : boolean ;
28- runOutput : IntegrationRunOutput | null ;
24+ run : AnyRealtimeRun | undefined ;
2925}
3026
3127const severityOrder = { critical : 0 , high : 1 , medium : 2 , low : 3 , info : 4 } ;
3228
33- // Helper function to extract clean error messages from cloud provider errors
34- function extractCleanErrorMessage ( errorMessage : string ) : string {
35- try {
36- // Try to parse as JSON (GCP returns JSON blob)
37- const parsed = JSON . parse ( errorMessage ) ;
38-
39- // GCP error structure: { error: { message: "actual message" } }
40- if ( parsed . error ?. message ) {
41- return parsed . error . message ;
42- }
43- } catch {
44- // Not JSON, return original
45- }
46-
47- return errorMessage ;
48- }
49-
50- export function ResultsView ( {
51- findings,
52- scanTaskId,
53- scanAccessToken,
54- onRunScan,
55- isScanning,
56- runOutput,
57- } : ResultsViewProps ) {
58- // Track scan status with Trigger.dev hooks
59- const { run } = useRealtimeRun ( scanTaskId || '' , {
60- enabled : ! ! scanTaskId && ! ! scanAccessToken ,
61- accessToken : scanAccessToken || undefined ,
62- } ) ;
63-
64- const runStatus = run ?. status ;
29+ export function ResultsView ( { findings, onRunScan, isScanning, run } : ResultsViewProps ) {
6530 const [ selectedStatus , setSelectedStatus ] = useState < string > ( 'all' ) ;
6631 const [ selectedSeverity , setSelectedSeverity ] = useState < string > ( 'all' ) ;
67- const [ showSuccessBanner , setShowSuccessBanner ] = useState ( false ) ;
68- const [ showErrorBanner , setShowErrorBanner ] = useState ( false ) ;
69- const [ showOutputErrorBanner , setShowOutputErrorBanner ] = useState ( false ) ;
70-
71- const runOutputError = runOutput && ! runOutput . success ? runOutput : null ;
72- const isRunTerminal = isTerminalRunStatus ( runStatus ) ;
73- const scanSucceeded = isRunTerminal && ! runOutputError && isSuccessfulRunStatus ( runStatus ) ;
74- const runHasHardFailure = isRunTerminal && ( isFailureRunStatus ( runStatus ) || Boolean ( run ?. error ) ) ;
75- const outputErrorMessages =
76- runOutputError ?. errors && runOutputError . errors . length > 0
77- ? runOutputError . errors
78- : ( runOutputError ?. failedIntegrations ?. map (
79- ( integration ) => `${ integration . name } : ${ integration . error } ` ,
80- ) ?? [ ] ) ;
81-
82- // Show success banner when scan completes successfully, auto-hide after 5 seconds
83- useEffect ( ( ) => {
84- if ( scanSucceeded ) {
85- setShowSuccessBanner ( true ) ;
86- const timer = setTimeout ( ( ) => {
87- setShowSuccessBanner ( false ) ;
88- } , 5000 ) ;
89- return ( ) => clearTimeout ( timer ) ;
90- }
91- setShowSuccessBanner ( false ) ;
92- } , [ scanSucceeded ] ) ;
93-
94- // Auto-dismiss error banner after 30 seconds
95- useEffect ( ( ) => {
96- if ( runHasHardFailure ) {
97- setShowErrorBanner ( true ) ;
98- const timer = setTimeout ( ( ) => {
99- setShowErrorBanner ( false ) ;
100- } , 30000 ) ;
101- return ( ) => clearTimeout ( timer ) ;
102- }
103- setShowErrorBanner ( false ) ;
104- } , [ runHasHardFailure ] ) ;
10532
106- // Show output error banner when run completes with errors
107- useEffect ( ( ) => {
108- if ( runOutputError ) {
109- setShowOutputErrorBanner ( true ) ;
110- setShowSuccessBanner ( false ) ;
111- } else {
112- setShowOutputErrorBanner ( false ) ;
113- }
114- } , [ runOutputError ] ) ;
33+ const isCompleted = run ?. status === 'COMPLETED' ;
34+ const isFailed =
35+ run ?. status === 'FAILED' ||
36+ run ?. status === 'CRASHED' ||
37+ run ?. status === 'SYSTEM_FAILURE' ||
38+ run ?. status === 'TIMED_OUT' ||
39+ run ?. status === 'CANCELED' ;
40+
41+ const runOutput =
42+ run ?. output && typeof run . output === 'object' && 'success' in run . output
43+ ? ( run . output as {
44+ success : boolean ;
45+ errors ?: string [ ] ;
46+ failedIntegrations ?: Array < { name : string ; error : string } > ;
47+ } )
48+ : null ;
49+
50+ const hasOutputErrors = runOutput && ! runOutput . success ;
51+ const outputErrorMessages = hasOutputErrors
52+ ? ( runOutput . errors ?? runOutput . failedIntegrations ?. map ( ( i ) => `${ i . name } : ${ i . error } ` ) ?? [ ] )
53+ : [ ] ;
11554
116- // Get unique statuses and severities
11755 const uniqueStatuses = Array . from (
11856 new Set ( findings . map ( ( f ) => f . status ) . filter ( Boolean ) as string [ ] ) ,
11957 ) ;
12058 const uniqueSeverities = Array . from (
12159 new Set ( findings . map ( ( f ) => f . severity ) . filter ( Boolean ) as string [ ] ) ,
12260 ) ;
12361
124- // Filter findings
12562 const filteredFindings = findings . filter ( ( finding ) => {
12663 const matchesStatus = selectedStatus === 'all' || finding . status === selectedStatus ;
12764 const matchesSeverity = selectedSeverity === 'all' || finding . severity === selectedSeverity ;
12865 return matchesStatus && matchesSeverity ;
12966 } ) ;
13067
131- // Sort findings by severity (always)
132- const sortedFindings = [ ...filteredFindings ] . sort ( ( a , b ) => {
133- const severityA = a . severity
134- ? ( severityOrder [ a . severity . toLowerCase ( ) as keyof typeof severityOrder ] ?? 999 )
135- : 999 ;
136- const severityB = b . severity
137- ? ( severityOrder [ b . severity . toLowerCase ( ) as keyof typeof severityOrder ] ?? 999 )
138- : 999 ;
139- return severityA - severityB ;
140- } ) ;
68+ const sortedFindings = useMemo (
69+ ( ) =>
70+ [ ...filteredFindings ] . sort ( ( a , b ) => {
71+ const severityA = a . severity
72+ ? ( severityOrder [ a . severity . toLowerCase ( ) as keyof typeof severityOrder ] ?? 999 )
73+ : 999 ;
74+ const severityB = b . severity
75+ ? ( severityOrder [ b . severity . toLowerCase ( ) as keyof typeof severityOrder ] ?? 999 )
76+ : 999 ;
77+ return severityA - severityB ;
78+ } ) ,
79+ [ filteredFindings ] ,
80+ ) ;
14181
14282 return (
14383 < div className = "flex flex-col gap-6" >
144- { /* Scan Status Banner */ }
14584 { isScanning && (
14685 < div className = "bg-primary/10 flex items-center gap-3 rounded-lg border border-primary/20 p-4" >
14786 < Loader2 className = "text-primary h-5 w-5 animate-spin flex-shrink-0" />
@@ -154,99 +93,62 @@ export function ResultsView({
15493 </ div >
15594 ) }
15695
157- { showSuccessBanner && scanSucceeded && ! isScanning && (
96+ { isCompleted && ! isScanning && ! hasOutputErrors && (
15897 < div className = "bg-primary/10 flex items-center gap-3 rounded-lg border border-primary/20 p-4" >
15998 < CheckCircle2 className = "text-primary h-5 w-5 flex-shrink-0" />
16099 < div className = "flex-1" >
161100 < p className = "text-primary text-sm font-medium" > Scan completed</ p >
162101 < p className = "text-muted-foreground text-xs" > Results updated successfully</ p >
163102 </ div >
164- < Button
165- variant = "ghost"
166- size = "sm"
167- onClick = { ( ) => setShowSuccessBanner ( false ) }
168- className = "text-muted-foreground hover:text-foreground h-auto p-1"
169- >
170- < X className = "h-4 w-4" />
171- </ Button >
172103 </ div >
173104 ) }
174105
175- { /* Output error banner when run reports errors but job didn't crash */ }
176- { showOutputErrorBanner && runOutputError && ! isScanning && (
106+ { hasOutputErrors && ! isScanning && (
177107 < div className = "bg-destructive/10 flex items-start gap-3 rounded-lg border border-destructive/20 p-4" >
178108 < AlertTriangle className = "text-destructive h-5 w-5 flex-shrink-0" />
179109 < div className = "flex-1 space-y-1" >
180110 < p className = "text-destructive text-sm font-medium" > Scan completed with errors</ p >
181111 < ul className = "text-muted-foreground text-xs leading-relaxed" >
182112 { outputErrorMessages . slice ( 0 , 5 ) . map ( ( message , index ) => (
183- < li key = { ` ${ message } - ${ index } ` } > • { message } </ li >
113+ < li key = { index } > • { message } </ li >
184114 ) ) }
185115 { outputErrorMessages . length === 0 && (
186116 < li > Encountered an unknown error while processing integration results.</ li >
187117 ) }
188118 </ ul >
189- { runOutputError . failedIntegrations && runOutputError . failedIntegrations . length > 0 && (
190- < p className = "text-muted-foreground text-xs" >
191- { runOutputError . failedIntegrations . length } integration
192- { runOutputError . failedIntegrations . length === 1 ? '' : 's' } returned errors.
193- </ p >
194- ) }
195119 </ div >
196- < Button
197- variant = "ghost"
198- size = "sm"
199- onClick = { ( ) => setShowOutputErrorBanner ( false ) }
200- className = "text-muted-foreground hover:text-foreground h-auto p-1"
201- >
202- < X className = "h-4 w-4" />
203- </ Button >
204120 </ div >
205121 ) }
206122
207- { /* Propagation delay info banner - only when scan succeeds but returns empty output */ }
208- { scanSucceeded && findings . length === 0 && ! isScanning && ! runOutputError && (
209- < 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" >
210- < Info className = "text-blue-600 dark:text-blue-400 h-5 w-5 flex-shrink-0" />
123+ { isFailed && ! isScanning && (
124+ < div className = "bg-destructive/10 flex items-center gap-3 rounded-lg border border-destructive/20 p-4" >
125+ < X className = "text-destructive h-5 w-5 flex-shrink-0" />
211126 < div className = "flex-1" >
212- < p className = "text-blue-900 dark:text-blue-100 text-sm font-medium" >
213- Initial scan complete
214- </ p >
127+ < p className = "text-destructive text-sm font-medium" > Scan failed</ p >
215128 < p className = "text-muted-foreground text-xs" >
216- Security findings may take 24-48 hours to appear after enabling cloud security
217- services. Check back later.
129+ { typeof run ?. error === 'object' && run . error && 'message' in run . error
130+ ? String ( run . error . message )
131+ : 'An error occurred during the scan. Please try again.' }
218132 </ p >
219133 </ div >
220134 </ div >
221135 ) }
222136
223- { showErrorBanner && runHasHardFailure && ! isScanning && (
224- < div className = "bg-destructive/10 flex items-center gap-3 rounded-lg border border-destructive/20 p-4" >
225- < X className = "text-destructive h-5 w-5 flex-shrink-0" />
137+ { isCompleted && findings . length === 0 && ! isScanning && ! hasOutputErrors && (
138+ < 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" >
139+ < Info className = "text-blue-600 dark:text-blue-400 h-5 w-5 flex-shrink-0" />
226140 < div className = "flex-1" >
227- < p className = "text-destructive text-sm font-medium" > Scan failed</ p >
141+ < p className = "text-blue-900 dark:text-blue-100 text-sm font-medium" >
142+ Initial scan complete
143+ </ p >
228144 < p className = "text-muted-foreground text-xs" >
229- { extractCleanErrorMessage (
230- ( typeof run ?. error === 'string' && run . error ) ||
231- ( run ?. error && typeof run . error === 'object' && 'message' in run . error
232- ? String ( ( run . error as { message ?: unknown } ) . message )
233- : undefined ) ||
234- 'An error occurred during the scan. Please try again.' ,
235- ) }
145+ Security findings may take 24-48 hours to appear after enabling cloud security
146+ services. Check back later.
236147 </ p >
237148 </ div >
238- < Button
239- variant = "ghost"
240- size = "sm"
241- onClick = { ( ) => setShowErrorBanner ( false ) }
242- className = "text-muted-foreground hover:text-foreground h-auto p-1"
243- >
244- < X className = "h-4 w-4" />
245- </ Button >
246149 </ div >
247150 ) }
248151
249- { /* Filters and Run Scan Button */ }
250152 < div className = "flex items-center justify-between" >
251153 { findings . length > 0 ? (
252154 < div className = "flex flex-wrap items-center gap-2" >
@@ -294,7 +196,6 @@ export function ResultsView({
294196 </ Button >
295197 </ div >
296198
297- { /* Results Table */ }
298199 { sortedFindings . length > 0 ? (
299200 < FindingsTable findings = { sortedFindings } />
300201 ) : findings . length > 0 ? (
0 commit comments