Skip to content

Commit e260fa4

Browse files
committed
fix: open status for accordions
1 parent 0008ef0 commit e260fa4

File tree

3 files changed

+106
-91
lines changed

3 files changed

+106
-91
lines changed

src/Shared/Components/CICDHistory/LogsRenderer.tsx

Lines changed: 92 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717
import { useParams } from 'react-router'
1818
import { useEffect, useRef, useState } from 'react'
1919
import AnsiUp from 'ansi_up'
20+
import { ANSI_UP_REGEX } from '@Shared/constants'
2021
import {
2122
Progressing,
2223
Host,
2324
useInterval,
2425
DOCUMENTATION,
2526
ROUTES,
26-
showError,
2727
SearchBar,
2828
ZERO_TIME_STRING,
2929
useUrlFilters,
@@ -32,6 +32,7 @@ import {
3232
import LogStageAccordion from './LogStageAccordion'
3333
import { EVENT_STREAM_EVENTS_MAP, LOGS_RETRY_COUNT, LOGS_STAGE_IDENTIFIER, POD_STATUS } from './constants'
3434
import {
35+
CreateMarkupReturnType,
3536
DeploymentHistoryBaseParamsType,
3637
HistoryComponentType,
3738
LogsRendererType,
@@ -177,33 +178,26 @@ export const LogsRenderer = ({
177178
const [logsList, setLogsList] = useState<string[]>([])
178179
const { searchKey, handleSearch } = useUrlFilters()
179180

180-
const areStagesAvailable = streamDataList?.[0]?.startsWith(LOGS_STAGE_IDENTIFIER)
181+
const areStagesAvailable = streamDataList[0]?.startsWith(LOGS_STAGE_IDENTIFIER) || false
181182

182-
function createMarkup(log: string): {
183-
__html: string
184-
isSearchKeyPresent: boolean
185-
} {
183+
function createMarkup(log: string, targetSearchKey: string = searchKey): CreateMarkupReturnType {
186184
let isSearchKeyPresent = false
187185
try {
188186
// eslint-disable-next-line no-param-reassign
189187
log = log.replace(/\[[.]*m/, (m) => `\x1B[${m}m`)
190188

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)
198192
const parts = logParts.reduce((acc, part, index) => {
199193
try {
200194
acc.push(
201195
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] : ''}`,
204198
),
205199
)
206-
if (part.includes(searchKey)) {
200+
if (part.includes(targetSearchKey)) {
207201
isSearchKeyPresent = true
208202
}
209203
} catch (searchRegexError) {
@@ -225,109 +219,92 @@ export const LogsRenderer = ({
225219
}
226220
}
227221

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
232240
}
233241

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
238244
}
239245

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+
}
284248

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] = {}
289260
}
290-
261+
acc[stageDetails.stage][stageDetails.startTime] = stageDetails
291262
return acc
292-
}, [] as StageDetailType[])
263+
},
264+
{} as Record<string, Record<string, StageDetailType>>,
265+
)
293266

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>> = {}
297269

298-
const newStageList = streamDataList.reduce((acc, streamItem: string, index) => {
270+
return streamDataList.reduce((acc, streamItem: string, index) => {
299271
if (streamItem.startsWith(LOGS_STAGE_IDENTIFIER)) {
300272
try {
301273
const { stage, startTime, endTime, status }: StageInfoDTO = JSON.parse(streamItem.split('|')[1])
302274
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]
306276

307277
if (existingStage) {
308278
// Would update the existing stage with new endTime
309279
existingStage.endTime = endTime
310280
existingStage.status = status
281+
existingStage.isOpen = getIsStageOpen(
282+
status,
283+
previousExistingStage?.isOpen,
284+
!!searchKeyStatusMap[stage]?.[startTime],
285+
!!targetSearchKey,
286+
)
311287
} else {
312288
acc.push({
313289
stage: stage || `Untitled stage ${index + 1}`,
314290
startTime: startTime || ZERO_TIME_STRING,
315291
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,
317295
logs: [],
318-
status,
319296
})
320297
}
321298
return acc
322299
} catch (e) {
323-
showError('Error while parsing logs stage')
324300
acc.push({
325-
stage: `Error ${index}`,
301+
stage: `Untitled stage ${index + 1}`,
326302
startTime: ZERO_TIME_STRING,
327303
endTime: ZERO_TIME_STRING,
328304
isOpen: false,
329305
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,
331308
})
332309
return acc
333310
}
@@ -336,21 +313,46 @@ export const LogsRenderer = ({
336313
// Ideally in case of parallel build should receive stage name with logs
337314
// NOTE: For now would always append log to last stage, can show a loader on stage tiles till processed
338315
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)
341321
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
343327
}
344328
}
345329

346330
return acc
347331
}, [] 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+
}
348344

345+
const newStageList = getStageListFromStreamData()
349346
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)])
351351

352352
const handleSearchEnter = (searchText: string) => {
353353
handleSearch(searchText)
354+
const newStageList = getStageListFromStreamData(searchText)
355+
setStageList(newStageList)
354356
}
355357

356358
const handleStageClose = (index: number) => {

src/Shared/Components/CICDHistory/types.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,13 +675,17 @@ export interface TriggerHistoryFilterCriteriaProps {
675675
export enum StageStatusType {
676676
SUCCESS = 'Success',
677677
FAILURE = 'Failure',
678+
/**
679+
* Not given in API response
680+
*/
681+
PROGRESSING = 'Progressing',
678682
}
679683

680684
export interface StageInfoDTO {
681685
stage: string
682686
startTime: string
683687
endTime?: string
684-
status: StageStatusType
688+
status?: StageStatusType
685689
}
686690

687691
export interface StageDetailType extends Pick<StageInfoDTO, 'stage' | 'startTime' | 'endTime' | 'status'> {
@@ -695,6 +699,11 @@ export interface LogStageAccordionProps extends StageDetailType {
695699
accordionIndex: number
696700
}
697701

702+
export interface CreateMarkupReturnType {
703+
__html: string
704+
isSearchKeyPresent: boolean
705+
}
706+
698707
export type TriggerHistoryFilterCriteriaType = `${string}|${string}|${string}`[]
699708
export const terminalStatus = new Set(['error', 'healthy', 'succeeded', 'cancelled', 'failed', 'aborted'])
700709
export const statusSet = new Set(['starting', 'running', 'pending'])

src/Shared/constants.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,3 +410,7 @@ export enum K8sResourcePayloadAppType {
410410
HELM_APP = 1,
411411
EXTERNAL_ARGO_APP = 2,
412412
}
413+
414+
// Disallowing this rule since ansi specifically works with escape characters
415+
// eslint-disable-next-line no-control-regex
416+
export const ANSI_UP_REGEX = /\x1B\[.*?m/g

0 commit comments

Comments
 (0)