feat: [CDE-141]: handle failure to parse container logs (#2215)

* feat: [CDE-141]: fixed time format in event timeline
* feat: [CDE-141]: handle failure to parse container logs
* feat: [CDE-141]: handle failure to parse container logs
unified-ui
Deepesh Kumar 2024-07-13 13:07:10 +00:00 committed by Harness
parent b9d3bb4d2b
commit 227cb55749
8 changed files with 89 additions and 30 deletions

View File

@ -7,7 +7,7 @@ import { useStrings } from 'framework/strings'
import { formatTimestamp } from './EventTimeline.utils'
import css from './EventTimeline.module.scss'
const EventTimeline = ({ data }: { data?: TypesGitspaceEventResponse[] | null; polling?: boolean }) => {
const EventTimeline = ({ data }: { data?: TypesGitspaceEventResponse[] | null }) => {
const localRef = useRef<HTMLDivElement | null>(null)
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
@ -50,7 +50,7 @@ const EventTimeline = ({ data }: { data?: TypesGitspaceEventResponse[] | null; p
<Container ref={localRef}>
{data?.map((item, index) => {
return (
<Layout.Horizontal background={Color.GREY_50} key={item.query_key}>
<Layout.Horizontal background={Color.GREY_50} key={`${item.query_key}_${item.timestamp}`}>
<Container
background={Color.GREY_50}
width={'8%'}

View File

@ -3,8 +3,17 @@ import moment from 'moment'
import { Text, Layout } from '@harnessio/uicore'
import { Color } from '@harnessio/design-system'
export const convertToMilliSecs = (timestamp: number) => {
if (timestamp > 1e12) {
timestamp = Math.floor(timestamp / 1e6)
}
return timestamp
}
export const formatTimestamp = (timestamp: number) => {
const inputDate = moment(timestamp)
const convertedTimeStamp = convertToMilliSecs(timestamp)
const inputDate = moment(convertedTimeStamp)
const currentDate = moment()
if (inputDate.isSame(currentDate, 'day')) {

View File

@ -3,9 +3,11 @@ import moment from 'moment'
import { Container, Text, Layout } from '@harnessio/uicore'
import { Color, FontVariation } from '@harnessio/design-system'
import { useStrings } from 'framework/strings'
import { convertToMilliSecs } from '../EventTimeline/EventTimeline.utils'
const EventTimelineSummary = ({ timestamp, message }: { timestamp?: number; message?: string }) => {
const { getString } = useStrings()
const convertedTimeStamp = convertToMilliSecs(timestamp || 0)
return (
<Container width="100%" flex={{ alignItems: 'center', justifyContent: 'space-between' }}>
<Text font={{ variation: FontVariation.CARD_TITLE }} margin={{ left: 'large' }}>
@ -17,7 +19,7 @@ const EventTimelineSummary = ({ timestamp, message }: { timestamp?: number; mess
{message}
</Text>
<Text margin={{ left: 'large' }} font={{ size: 'small' }}>
{moment(timestamp).format('DD MMM, YYYY hh:mma')}
{moment(convertedTimeStamp).format('DD MMM, YYYY hh:mma')}
</Text>
</Layout.Horizontal>
)}

View File

@ -539,8 +539,10 @@ export const RenderActions = ({ row, refreshList }: RenderActionsProps) => {
const handleDelete = async (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
confirmDelete({
title: getString('cde.deleteGitspaceTitle'),
message: getString('cde.deleteGitspaceText', { name: name }),
intent: 'danger',
title: getString('cde.deleteGitspaceTitle', { name: name }),
message: getString('cde.deleteGitspaceText'),
confirmText: getString('delete'),
action: async () => {
try {
e.preventDefault()

View File

@ -1,4 +1,6 @@
.formFields {
div[data-id='branch-1'],
div[data-id='code_repo_url-0'],
[class*='repoInput'],
[class*='branchDropdown'] {
width: 100% !important;

View File

@ -1,4 +1,6 @@
import { get } from 'lodash-es'
import { useEffect, useState } from 'react'
import { useStrings, type UseStringsReturn } from 'framework/strings'
export interface LogData {
pos: number
@ -6,24 +8,29 @@ export interface LogData {
time: number
}
export function parseLog(log: string): LogData[] {
export function parseLog(log: string, getString: UseStringsReturn['getString']): LogData[] {
const logLines = log.trim().split('\n\n')
const parsedData: LogData[] = []
logLines.forEach(line => {
const dataMatch = line.match(/data: (.+)/)
try {
logLines.forEach(line => {
const dataMatch = line.match(/data: (.+)/)
if (dataMatch && dataMatch[1] !== 'eof') {
const eventData: LogData = JSON.parse(dataMatch[1])
if (dataMatch && dataMatch[1] !== 'eof') {
const eventData: LogData = JSON.parse(dataMatch[1])
parsedData.push(eventData)
}
})
parsedData.push(eventData)
}
})
} catch (error) {
parsedData.push({ pos: 1, out: get(error, 'message') || getString('cde.details.logsFailed'), time: 0 })
}
return parsedData
}
export const useGetLogStream = ({ response }: { response: any }) => {
const { getString } = useStrings()
const [data, setData] = useState('')
useEffect(() => {
@ -50,5 +57,5 @@ export const useGetLogStream = ({ response }: { response: any }) => {
}
}, [response])
return { data: parseLog(data) }
return { data: parseLog(data, getString) }
}

View File

@ -22,7 +22,7 @@ import { useAppContext } from 'AppContext'
import { useStrings } from 'framework/strings'
import EventTimelineAccordion from 'cde-gitness/components/EventTimelineAccordion/EventTimelineAccordion'
import { DetailsCard } from 'cde-gitness/components/DetailsCard/DetailsCard'
import type { TypesGitspaceConfig, TypesGitspaceEventResponse } from 'cde-gitness/services'
import type { EnumGitspaceStateType, TypesGitspaceConfig, TypesGitspaceEventResponse } from 'cde-gitness/services'
import { GitspaceActionType, GitspaceStatus } from 'cde/constants'
import { useQueryParams } from 'hooks/useQueryParams'
import { useUpdateQueryParams } from 'hooks/useUpdateQueryParams'
@ -33,6 +33,7 @@ import vscodeIcon from 'cde/icons/VSCode.svg?url'
import pauseIcon from 'cde-gitness/assests/pause.svg?url'
import { StandaloneIDEType } from 'cde-gitness/constants'
import homeIcon from 'cde-gitness/assests/home.svg?url'
import { useConfirmAct } from 'hooks/useConfirmAction'
import ContainerLogs from '../../components/ContainerLogs/ContainerLogs'
import { useGetLogStream } from '../../hooks/useGetLogStream'
import css from './GitspaceDetails.module.scss'
@ -41,9 +42,10 @@ export const GitspaceDetails = () => {
const space = useGetSpaceParam()
const { getString } = useStrings()
const { routes } = useAppContext()
const { showError } = useToaster()
const { showError, showSuccess } = useToaster()
const history = useHistory()
const [startTriggred, setStartTriggred] = useState<boolean>(false)
const [triggerPollingOnStart, setTriggerPollingOnStart] = useState<EnumGitspaceStateType>()
const { gitspaceId = '' } = useParams<{ gitspaceId?: string }>()
const [isStreamingLogs, setIsStreamingLogs] = useState(false)
@ -60,7 +62,11 @@ export const GitspaceDetails = () => {
debounce: 500
})
const { refetch: refetchLogsData, response } = useGet<any>({
const {
refetch: refetchLogsData,
response,
error: streamLogsError
} = useGet<any>({
path: `api/v1/gitspaces/${space}/${gitspaceId}/+/logs/stream`,
debounce: 500,
lazy: true
@ -79,12 +85,11 @@ export const GitspaceDetails = () => {
const { updateQueryParams } = useUpdateQueryParams<{ redirectFrom?: string }>()
const { redirectFrom = '' } = useQueryParams<{ redirectFrom?: string }>()
const pollingCondition = [
GitspaceStatus.RUNNING,
GitspaceStatus.STOPPED,
GitspaceStatus.ERROR,
GitspaceStatus.UNINITIALIZED
].includes(data?.state as GitspaceStatus)
const pollingCondition = triggerPollingOnStart
? false
: [GitspaceStatus.RUNNING, GitspaceStatus.STOPPED, GitspaceStatus.ERROR, GitspaceStatus.UNINITIALIZED].includes(
data?.state as GitspaceStatus
)
const disabledActionButtons = [GitspaceStatus.STARTING, GitspaceStatus.STOPPING].includes(
data?.state as GitspaceStatus
@ -99,15 +104,21 @@ export const GitspaceDetails = () => {
if (disabledActionButtons && filteredEvent?.length && !isStreamingLogs) {
refetchLogsData()
setIsStreamingLogs(true)
} else if (filteredEvent?.length && !disabledActionButtons && isStreamingLogs) {
} else if (
(filteredEvent?.length && !disabledActionButtons && isStreamingLogs) ||
(isStreamingLogs && streamLogsError)
) {
setIsStreamingLogs(false)
}
}, [eventData, data?.instance?.updated, disabledActionButtons])
}, [eventData, data?.instance?.updated, disabledActionButtons, streamLogsError])
usePolling(
async () => {
await refetchEventData()
await refetch()
if (triggerPollingOnStart) {
setTriggerPollingOnStart(undefined)
}
},
{
pollingInterval: 10000,
@ -120,7 +131,11 @@ export const GitspaceDetails = () => {
if (redirectFrom && !startTriggred && !mutateLoading) {
try {
setStartTriggred(true)
await actionMutate({ action: GitspaceActionType.START })
const resp = await actionMutate({ action: GitspaceActionType.START })
if (resp?.state === GitspaceStatus.STARTING) {
setTriggerPollingOnStart(resp.state)
}
await refetchEventData()
await refetch()
updateQueryParams({ redirectFrom: undefined })
} catch (err) {
@ -136,6 +151,28 @@ export const GitspaceDetails = () => {
const formattedlogsdata = useGetLogStream({ response })
const confirmDelete = useConfirmAct()
const handleDelete = async (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
confirmDelete({
intent: 'danger',
title: getString('cde.deleteGitspaceTitle', { name: data?.name }),
message: getString('cde.deleteGitspaceText'),
confirmText: getString('delete'),
action: async () => {
try {
e.preventDefault()
e.stopPropagation()
await deleteGitspace({})
showSuccess(getString('cde.deleteSuccess'))
history.push(routes.toCDEGitspaces({ space }))
} catch (exception) {
showError(getErrorMessage(exception))
}
}
})
}
return (
<>
<Page.Header
@ -223,7 +260,7 @@ export const GitspaceDetails = () => {
}}
/>
<MenuItem
onClick={deleteGitspace as Unknown as () => void}
onClick={handleDelete as Unknown as () => void}
text={
<Layout.Horizontal
spacing="small"

View File

@ -1160,8 +1160,8 @@ cde:
stopingGitspace: Stopping Gitspace
sessionDuration: Last Started
lastActivated: LAST ACTIVATED
deleteGitspaceTitle: Delete Gitspace
deleteGitspaceText: "Are you sure to delete the gitspace, '{{name}}'?"
deleteGitspaceTitle: Delete Gitspace '{{name}}'
deleteGitspaceText: 'This action cannot be undone. Are you sure you want to proceed to delete?'
deleteSuccess: Gitspace deleted succesfully
repositoryAndBranch: Repository & Branch
importInto: Import Repository into Gitness