1
- import { Box , Typography } from '@mui/material'
1
+ import { Box , Table , TableBody , TableCell , TableHead , TableRow , Typography } from '@mui/material'
2
+ import { orderBy } from 'lodash'
2
3
import { useEffect , useReducer } from 'react'
4
+ import { IngestionPipelineStageKey , IngestionPipelineStageKeys , IngestionPipelineStages } from '../../../shared/constants'
3
5
4
6
type ProgressEvent = {
5
7
stage : string
6
- item ?: string
8
+ items ?: string [ ]
7
9
done ?: boolean
8
10
error ?: string
9
11
}
10
12
11
- type ProgressState = {
12
- [ fileName : string ] : {
13
- [ stage : string ] : {
14
- count : number
15
- done : boolean
13
+ type ProgressState = Record <
14
+ IngestionPipelineStageKey ,
15
+ {
16
+ count : number
17
+ done : boolean
18
+ error : boolean
19
+ files : {
20
+ [ fileName : string ] : {
21
+ count : number
22
+ done : boolean
23
+ error : boolean
24
+ }
16
25
}
17
26
}
18
- }
27
+ >
28
+
29
+ const getInitialProgressState = ( ) =>
30
+ Object . fromEntries (
31
+ IngestionPipelineStageKeys . map ( ( stage ) => [
32
+ stage ,
33
+ {
34
+ count : 0 ,
35
+ done : false ,
36
+ error : false ,
37
+ files : { } ,
38
+ } ,
39
+ ] ) ,
40
+ ) as ProgressState
19
41
20
42
type Action = { type : 'UPDATE' ; payload : ProgressEvent } | { type : 'RESET' }
21
43
22
44
const progressReducer = ( state : ProgressState , action : Action ) : ProgressState => {
23
45
switch ( action . type ) {
24
46
case 'UPDATE' : {
25
- const { stage, item, done } = action . payload
47
+ const { stage, items, done, error } = action . payload
48
+
26
49
if ( done ) {
27
- // mark all items at this stage as done
28
- const newState = { ...state }
29
- for ( const file in newState ) {
30
- if ( newState [ file ] [ stage ] ) {
31
- newState [ file ] [ stage ] . done = true
32
- }
50
+ return {
51
+ ...state ,
52
+ [ stage ] : {
53
+ ...state [ stage ] ,
54
+ done : true ,
55
+ error : ! ! error || false ,
56
+ } ,
33
57
}
34
- return newState
35
58
}
36
- if ( ! item ) return state
37
59
38
- const fileStages = state [ item ] || { }
39
- const stageData = fileStages [ stage ] || { count : 0 , done : false }
60
+ if ( ! items ) return state
61
+
62
+ const updatedFiles = items . reduce (
63
+ ( acc , fileName ) => {
64
+ if ( ! acc [ fileName ] ) {
65
+ acc [ fileName ] = { count : 0 , done : done || false , error : ! ! error || false }
66
+ }
67
+ acc [ fileName ] . count += 1
68
+ return acc
69
+ } ,
70
+ { ...state [ stage ] . files } ,
71
+ )
40
72
41
73
return {
42
74
...state ,
43
- [ item ] : {
44
- ...fileStages ,
45
- [ stage ] : {
46
- count : stageData . count + 1 ,
47
- done : stageData . done || false ,
48
- } ,
75
+ [ stage ] : {
76
+ ...state [ stage ] ,
77
+ done : done || state [ stage ] ?. done ,
78
+ error : ! ! error || state [ stage ] ?. error ,
79
+ count : state [ stage ] ?. count + items . length ,
80
+ files : updatedFiles ,
49
81
} ,
50
82
}
51
83
}
52
84
53
85
case 'RESET' :
54
- return { }
86
+ return getInitialProgressState ( )
55
87
56
88
default :
57
89
return state
58
90
}
59
91
}
60
92
61
- export const ProgressReporter : React . FC < { stream : ReadableStream | null } > = ( { stream } ) => {
62
- const [ progress , dispatch ] = useReducer ( progressReducer , { } )
93
+ export const ProgressReporter : React . FC < { filenames : string [ ] ; stream : ReadableStream | null ; onError : ( ) => void } > = ( { filenames , stream, onError } ) => {
94
+ const [ progress , dispatch ] = useReducer ( progressReducer , getInitialProgressState ( ) )
63
95
64
96
useEffect ( ( ) => {
65
97
let reader : ReadableStreamDefaultReader < Uint8Array > | null = null
@@ -69,6 +101,8 @@ export const ProgressReporter: React.FC<{ stream: ReadableStream | null }> = ({
69
101
reader = stream . getReader ( )
70
102
} catch ( error ) {
71
103
console . error ( 'Error getting reader from stream:' , error )
104
+ dispatch ( { type : 'RESET' } )
105
+ onError ( )
72
106
return
73
107
}
74
108
@@ -90,36 +124,79 @@ export const ProgressReporter: React.FC<{ stream: ReadableStream | null }> = ({
90
124
dispatch ( { type : 'UPDATE' , payload : jsonChunk } )
91
125
} catch ( err ) {
92
126
console . error ( 'Invalid chunk:' , line , err )
127
+ onError ( )
93
128
}
94
129
}
95
130
}
96
131
}
97
132
}
98
133
99
134
readStream ( )
135
+ } else {
136
+ dispatch ( { type : 'RESET' } )
137
+ }
100
138
101
- return ( ) => {
102
- if ( reader ) {
103
- reader . releaseLock ( )
104
- }
139
+ return ( ) => {
140
+ if ( reader ) {
141
+ reader . releaseLock ( )
142
+ dispatch ( { type : 'RESET' } )
105
143
}
106
144
}
107
145
} , [ stream ] )
108
146
147
+ console . log ( 'Progress state:' , progress )
148
+
109
149
return (
110
- < Box >
111
- { Object . entries ( progress ) . map ( ( [ file , stages ] ) => (
112
- < div key = { file } className = "border p-2 rounded shadow" >
113
- < h3 className = "font-bold text-lg" > { file } </ h3 >
114
- < ul className = "ml-4 list-disc" >
115
- { Object . entries ( stages ) . map ( ( [ stage , { count, done } ] ) => (
116
- < li key = { stage } >
117
- < span className = "font-medium" > { stage } :</ span > { done ? "✅ Done" : `🔄 Processing (${ count } x)` }
118
- </ li >
150
+ < Table size = "small" >
151
+ < TableHead >
152
+ < TableRow >
153
+ < TableCell > File</ TableCell >
154
+ { IngestionPipelineStageKeys . map ( ( stage ) => (
155
+ < TableCell key = { stage } >
156
+ < Typography variant = "body2" > { IngestionPipelineStages [ stage ] . name } </ Typography >
157
+ < Typography variant = "caption" color = "textSecondary" >
158
+ { progress [ stage ] . count } { ' ' }
159
+ </ Typography >
160
+ < Typography variant = "caption" color = "textSecondary" >
161
+ { progress [ stage ] . done ? 'Done' : progress [ stage ] . error ? 'Error' : 'In Progress' }
162
+ </ Typography >
163
+ </ TableCell >
164
+ ) ) }
165
+ </ TableRow >
166
+ </ TableHead >
167
+ < TableBody >
168
+ { filenames . map ( ( filename ) => (
169
+ < TableRow key = { filename } >
170
+ < TableCell component = "th" scope = "row" >
171
+ { filename }
172
+ </ TableCell >
173
+ { IngestionPipelineStageKeys . map ( ( stage ) => (
174
+ < TableCell
175
+ key = { stage }
176
+ sx = { {
177
+ transition : 'background-color 0.3s' ,
178
+ backgroundColor : progress [ stage ] ?. error
179
+ ? 'error.light'
180
+ : progress [ stage ] ?. done
181
+ ? 'success.light'
182
+ : progress [ stage ] ?. files [ filename ] ?. error
183
+ ? 'error.light'
184
+ : progress [ stage ] ?. files [ filename ] ?. count
185
+ ? 'info.light'
186
+ : 'inherit' ,
187
+ } }
188
+ >
189
+ < Box display = "flex" gap = { 2 } >
190
+ < Typography variant = "body2" > { progress [ stage ] . files [ filename ] ?. count > 1 || '' } </ Typography >
191
+ < Typography variant = "caption" color = "textSecondary" >
192
+ { progress [ stage ] ?. files [ filename ] ?. done ? 'Done' : progress [ stage ] . files [ filename ] ?. error ? 'Error' : '' }
193
+ </ Typography >
194
+ </ Box >
195
+ </ TableCell >
119
196
) ) }
120
- </ ul >
121
- </ div >
122
- ) ) }
123
- </ Box >
197
+ </ TableRow >
198
+ ) ) }
199
+ </ TableBody >
200
+ </ Table >
124
201
)
125
202
}
0 commit comments