17
17
import { useParams } from 'react-router'
18
18
import { useEffect , useRef , useState } from 'react'
19
19
import AnsiUp from 'ansi_up'
20
+ import { ANSI_UP_REGEX } from '@Shared/constants'
20
21
import {
21
22
Progressing ,
22
23
Host ,
23
24
useInterval ,
24
25
DOCUMENTATION ,
25
26
ROUTES ,
26
- showError ,
27
27
SearchBar ,
28
28
ZERO_TIME_STRING ,
29
29
useUrlFilters ,
@@ -32,6 +32,7 @@ import {
32
32
import LogStageAccordion from './LogStageAccordion'
33
33
import { EVENT_STREAM_EVENTS_MAP , LOGS_RETRY_COUNT , LOGS_STAGE_IDENTIFIER , POD_STATUS } from './constants'
34
34
import {
35
+ CreateMarkupReturnType ,
35
36
DeploymentHistoryBaseParamsType ,
36
37
HistoryComponentType ,
37
38
LogsRendererType ,
@@ -177,33 +178,26 @@ export const LogsRenderer = ({
177
178
const [ logsList , setLogsList ] = useState < string [ ] > ( [ ] )
178
179
const { searchKey, handleSearch } = useUrlFilters ( )
179
180
180
- const areStagesAvailable = streamDataList ?. [ 0 ] ?. startsWith ( LOGS_STAGE_IDENTIFIER )
181
+ const areStagesAvailable = streamDataList [ 0 ] ?. startsWith ( LOGS_STAGE_IDENTIFIER ) || false
181
182
182
- function createMarkup ( log : string ) : {
183
- __html : string
184
- isSearchKeyPresent : boolean
185
- } {
183
+ function createMarkup ( log : string , targetSearchKey : string = searchKey ) : CreateMarkupReturnType {
186
184
let isSearchKeyPresent = false
187
185
try {
188
186
// eslint-disable-next-line no-param-reassign
189
187
log = log . replace ( / \[ [ . ] * m / , ( m ) => `\x1B[${ m } m` )
190
188
191
- if ( searchKey && areStagesAvailable ) {
192
- // Disallowing this rule since ansi specifically works with escape characters
193
- // eslint-disable-next-line no-control-regex
194
- const ansiRegex = / \x1B \[ .* ?m / g
195
-
196
- const logParts = log . split ( ansiRegex )
197
- const availableEscapeCodes = log . match ( ansiRegex )
189
+ if ( targetSearchKey && areStagesAvailable ) {
190
+ const logParts = log . split ( ANSI_UP_REGEX )
191
+ const availableEscapeCodes = log . match ( ANSI_UP_REGEX )
198
192
const parts = logParts . reduce ( ( acc , part , index ) => {
199
193
try {
200
194
acc . push (
201
195
part . replace (
202
- new RegExp ( searchKey , 'g' ) ,
203
- `\x1B[48;2;197;141;54m${ searchKey } \x1B[0m${ index > 0 ? availableEscapeCodes [ index - 1 ] : '' } ` ,
196
+ new RegExp ( targetSearchKey , 'g' ) ,
197
+ `\x1B[48;2;197;141;54m${ targetSearchKey } \x1B[0m${ index > 0 ? availableEscapeCodes [ index - 1 ] : '' } ` ,
204
198
) ,
205
199
)
206
- if ( part . includes ( searchKey ) ) {
200
+ if ( part . includes ( targetSearchKey ) ) {
207
201
isSearchKeyPresent = true
208
202
}
209
203
} catch ( searchRegexError ) {
@@ -225,109 +219,92 @@ export const LogsRenderer = ({
225
219
}
226
220
}
227
221
228
- // TODO: Look into code duplication later
229
- useEffect ( ( ) => {
230
- if ( ! streamDataList ?. length ) {
231
- return
222
+ /**
223
+ *
224
+ * @param status - status of the stage
225
+ * @param lastUserActionState - If true, user had opened the stage else closed the stage
226
+ * @param isSearchKeyPresent - If search key is present in the logs of that stage
227
+ * @param isFromSearchAction - If the action is from search action
228
+ * @returns
229
+ */
230
+ const getIsStageOpen = (
231
+ status : StageStatusType ,
232
+ lastUserActionState : boolean ,
233
+ isSearchKeyPresent : boolean ,
234
+ isFromSearchAction : boolean ,
235
+ ) : boolean => {
236
+ const isInitialState = stageList . length === 0
237
+
238
+ if ( isFromSearchAction ) {
239
+ return isSearchKeyPresent || lastUserActionState
232
240
}
233
241
234
- if ( ! areStagesAvailable ) {
235
- const newLogs = streamDataList . map ( ( logItem ) => createMarkup ( logItem ) . __html )
236
- setLogsList ( newLogs )
237
- return
242
+ if ( isInitialState ) {
243
+ return status !== StageStatusType . SUCCESS || isSearchKeyPresent
238
244
}
239
245
240
- // If initially parsedLogs are empty, and initialStatus is Success then would set opened as false on each
241
- // If initialStatus is not success and initial parsedLogs are empty then would set opened as false on each except the last
242
- if ( stageList . length === 0 ) {
243
- const newStageList : StageDetailType [ ] = streamDataList . reduce ( ( acc , streamItem : string , index ) => {
244
- if ( streamItem . startsWith ( LOGS_STAGE_IDENTIFIER ) ) {
245
- try {
246
- const { stage, startTime, endTime, status } : StageInfoDTO = JSON . parse ( streamItem . split ( '|' ) [ 1 ] )
247
- const existingStage = acc . find ( ( item ) => item . stage === stage && item . startTime === startTime )
248
- if ( existingStage ) {
249
- // Would update the existing stage with new endTime
250
- existingStage . endTime = endTime
251
- existingStage . status = status
252
- } else {
253
- acc . push ( {
254
- stage : stage || `Untitled stage ${ index + 1 } ` ,
255
- startTime : startTime || ZERO_TIME_STRING ,
256
- endTime : endTime || ZERO_TIME_STRING ,
257
- isOpen :
258
- status === StageStatusType . SUCCESS
259
- ? false
260
- : acc . length === streamDataList . length - 1 ,
261
- logs : [ ] ,
262
- status,
263
- } )
264
- }
265
- return acc
266
- } catch ( e ) {
267
- showError ( 'Error while parsing logs stage' )
268
- acc . push ( {
269
- stage : `Error ${ index } ` ,
270
- startTime : ZERO_TIME_STRING ,
271
- endTime : ZERO_TIME_STRING ,
272
- isOpen : false ,
273
- logs : [ ] ,
274
- status : StageStatusType . FAILURE ,
275
- } )
276
- return acc
277
- }
278
- }
279
-
280
- // Ideally in case of parallel build should receive stage name with logs
281
- // NOTE: For now would always append log to last stage, can show a loader on stage tiles till processed
282
- if ( acc . length > 0 ) {
283
- const { __html, isSearchKeyPresent } = createMarkup ( streamItem )
246
+ return lastUserActionState ?? true
247
+ }
284
248
285
- acc [ acc . length - 1 ] . logs . push ( __html )
286
- if ( isSearchKeyPresent ) {
287
- acc [ acc . length - 1 ] . isOpen = true
288
- }
249
+ /**
250
+ * If initially parsedLogs are empty, and initialStatus is Success then would set opened as false on each
251
+ * If initialStatus is not success and initial parsedLogs are empty then would set opened as false on each except the last
252
+ * In case data is already present we will just find user's last action else would open the stage
253
+ */
254
+ const getStageListFromStreamData = ( targetSearchKey ?: string ) : StageDetailType [ ] => {
255
+ // Would be using this to get last user action on stage
256
+ const previousStageMap : Readonly < Record < string , Readonly < Record < string , StageDetailType > > > > = stageList . reduce (
257
+ ( acc , stageDetails ) => {
258
+ if ( ! acc [ stageDetails . stage ] ) {
259
+ acc [ stageDetails . stage ] = { }
289
260
}
290
-
261
+ acc [ stageDetails . stage ] [ stageDetails . startTime ] = stageDetails
291
262
return acc
292
- } , [ ] as StageDetailType [ ] )
263
+ } ,
264
+ { } as Record < string , Record < string , StageDetailType > > ,
265
+ )
293
266
294
- setStageList ( newStageList )
295
- return
296
- }
267
+ // Map of stage as key and value as object with key as start time and value as boolean depicting if search key is present or not
268
+ const searchKeyStatusMap : Record < string , Record < string , boolean > > = { }
297
269
298
- const newStageList = streamDataList . reduce ( ( acc , streamItem : string , index ) => {
270
+ return streamDataList . reduce ( ( acc , streamItem : string , index ) => {
299
271
if ( streamItem . startsWith ( LOGS_STAGE_IDENTIFIER ) ) {
300
272
try {
301
273
const { stage, startTime, endTime, status } : StageInfoDTO = JSON . parse ( streamItem . split ( '|' ) [ 1 ] )
302
274
const existingStage = acc . find ( ( item ) => item . stage === stage && item . startTime === startTime )
303
- const previousExistingStage = stageList . find (
304
- ( item ) => item . stage === stage && item . startTime === startTime ,
305
- )
275
+ const previousExistingStage = previousStageMap [ stage ] ?. [ startTime ]
306
276
307
277
if ( existingStage ) {
308
278
// Would update the existing stage with new endTime
309
279
existingStage . endTime = endTime
310
280
existingStage . status = status
281
+ existingStage . isOpen = getIsStageOpen (
282
+ status ,
283
+ previousExistingStage ?. isOpen ,
284
+ ! ! searchKeyStatusMap [ stage ] ?. [ startTime ] ,
285
+ ! ! targetSearchKey ,
286
+ )
311
287
} else {
312
288
acc . push ( {
313
289
stage : stage || `Untitled stage ${ index + 1 } ` ,
314
290
startTime : startTime || ZERO_TIME_STRING ,
315
291
endTime : endTime || ZERO_TIME_STRING ,
316
- isOpen : previousExistingStage ? previousExistingStage . isOpen : true ,
292
+ // Would be defining the state when we receive the end status, otherwise it is loading and would be open
293
+ isOpen : true ,
294
+ status : StageStatusType . PROGRESSING ,
317
295
logs : [ ] ,
318
- status,
319
296
} )
320
297
}
321
298
return acc
322
299
} catch ( e ) {
323
- showError ( 'Error while parsing logs stage' )
324
300
acc . push ( {
325
- stage : `Error ${ index } ` ,
301
+ stage : `Untitled stage ${ index + 1 } ` ,
326
302
startTime : ZERO_TIME_STRING ,
327
303
endTime : ZERO_TIME_STRING ,
328
304
isOpen : false ,
329
305
logs : [ ] ,
330
- status : StageStatusType . FAILURE ,
306
+ // Can not set status as FAILURE, as we will append latest logs to last stage
307
+ status : StageStatusType . PROGRESSING ,
331
308
} )
332
309
return acc
333
310
}
@@ -336,21 +313,46 @@ export const LogsRenderer = ({
336
313
// Ideally in case of parallel build should receive stage name with logs
337
314
// NOTE: For now would always append log to last stage, can show a loader on stage tiles till processed
338
315
if ( acc . length > 0 ) {
339
- const { __html, isSearchKeyPresent } = createMarkup ( streamItem )
340
- acc [ acc . length - 1 ] . logs . push ( __html )
316
+ // In case targetSearchKey is not present createMarkup will internally fallback to searchKey
317
+ const { __html, isSearchKeyPresent } = createMarkup ( streamItem , targetSearchKey )
318
+
319
+ const lastStage = acc [ acc . length - 1 ]
320
+ lastStage . logs . push ( __html )
341
321
if ( isSearchKeyPresent ) {
342
- acc [ acc . length - 1 ] . isOpen = true
322
+ if ( ! searchKeyStatusMap [ lastStage . stage ] ) {
323
+ searchKeyStatusMap [ lastStage . stage ] = { }
324
+ }
325
+
326
+ searchKeyStatusMap [ lastStage . stage ] [ lastStage . startTime ] = true
343
327
}
344
328
}
345
329
346
330
return acc
347
331
} , [ ] as StageDetailType [ ] )
332
+ }
333
+
334
+ useEffect ( ( ) => {
335
+ if ( ! streamDataList ?. length ) {
336
+ return
337
+ }
338
+
339
+ if ( ! areStagesAvailable ) {
340
+ const newLogs = streamDataList . map ( ( logItem ) => createMarkup ( logItem ) . __html )
341
+ setLogsList ( newLogs )
342
+ return
343
+ }
348
344
345
+ const newStageList = getStageListFromStreamData ( )
349
346
setStageList ( newStageList )
350
- } , [ JSON . stringify ( streamDataList ) , searchKey ] )
347
+ // NOTE: Not adding searchKey as dependency since on mount we would already have searchKey
348
+ // And for other cases we would use handleSearchEnter
349
+ // TODO: Check if stringifying the streamDataList is required
350
+ } , [ JSON . stringify ( streamDataList ) ] )
351
351
352
352
const handleSearchEnter = ( searchText : string ) => {
353
353
handleSearch ( searchText )
354
+ const newStageList = getStageListFromStreamData ( searchText )
355
+ setStageList ( newStageList )
354
356
}
355
357
356
358
const handleStageClose = ( index : number ) => {
0 commit comments