Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions moon/apps/web/components/Issues/IssueDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ export default function IssueDetailPage({ link }: { link: string }) {
<UIText size='text-2xl' weight='font-bold' className='-tracking-[1px] lg:flex'>
{`${issueDetail?.title || ''}`}
<span>&nbsp;</span>
<span className='font-light !text-[#59636e]'>${issueDetail?.id}</span>
<span className='font-light !text-[#59636e]'>${link}</span>
</UIText>
<Button
onClick={() => {
Expand Down Expand Up @@ -384,7 +384,7 @@ export default function IssueDetailPage({ link }: { link: string }) {
<LoadingSpinner />
</div>
) : (
<TimelineItems detail={issueDetail} id={link} type='issue' editorRef={editorRef}/>
<TimelineItems detail={issueDetail} id={link} type='issue' editorRef={editorRef} />
)}

{info && info.status === 'open' && (
Expand Down
2 changes: 1 addition & 1 deletion moon/apps/web/components/Issues/utils/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ export const editIdAtom = atom(0)

export const refreshAtom = atom(0)

export const buildId = atom('')
export const buildIdAtom = atom('')
36 changes: 36 additions & 0 deletions moon/apps/web/components/Layout/TabLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { PropsWithChildren } from 'react'
import { ChecklistIcon, CommentDiscussionIcon, FileDiffIcon } from '@primer/octicons-react'
import { UnderlineNav } from '@primer/react'
import { useAtom } from 'jotai'

import { tabAtom } from '../MrView/components/Checks/cpns/store'

export const TabLayout = ({ children }: PropsWithChildren) => {
const [tab, setTab] = useAtom(tabAtom)

return (
<>
<UnderlineNav aria-label='Repository with leading icons'>
<UnderlineNav.Item
aria-selected={tab === 'conversation'}
onClick={() => setTab('conversation')}
icon={CommentDiscussionIcon}
// aria-current='page'
>
Conversation
</UnderlineNav.Item>
<UnderlineNav.Item aria-selected={tab === 'check'} onClick={() => setTab('check')} icon={ChecklistIcon}>
Checks
</UnderlineNav.Item>
<UnderlineNav.Item
aria-selected={tab === 'filechange'}
onClick={() => setTab('filechange')}
icon={FileDiffIcon}
>
Files Changed
</UnderlineNav.Item>
</UnderlineNav>
{children}
</>
)
}
84 changes: 49 additions & 35 deletions moon/apps/web/components/MrView/components/Checks/cpns/Task.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { useAtom } from 'jotai'

import { LoadingSpinner } from '@gitmono/ui/Spinner'

import { buildId } from '@/components/Issues/utils/store'
import { buildIdAtom } from '@/components/Issues/utils/store'
import { TaskResult } from '@/hooks/SSE/useGetMrTask'

import { loadingAtom, Status, statusAtom } from './store'
import { Status, statusMapAtom } from './store'

export const mocks = [
{
arguments: '--env=prod --force',
build_id: 'BUILD_20250813001',
build_id: '0198b32b-6ede-7be2-99dc-aee8c7ef358d',
end_at: '2025-08-13T16:20:00Z',
exit_code: 0,
mr: 'MR-125',
Expand Down Expand Up @@ -47,31 +47,8 @@ export const mocks = [

export const Task = ({ list }: { list: TaskResult[] }) => {
const [extend, setExtend] = useState(false)
const [_, setBuildId] = useAtom(buildId)
const [_loading, setLoading] = useAtom(loadingAtom)
const [status] = useAtom(statusAtom)

list = mocks

const handleClick = (build_id: string) => {
// 此处建立连接
setLoading(true)
setBuildId(build_id)
// if (eventSourcesRef.current[build_id]) return
// setEventSource(build_id)
}

const identifyStatus = (status: string) => {
switch (status) {
case Status.Success:
return <CheckIcon size={14} className='text-[#1a7f37]' />
case Status.Fail:
return <XIcon size={14} className='text-[#d53d46]' />

default:
return <LoadingSpinner />
}
}
// list = mocks

return (
<>
Expand All @@ -89,17 +66,54 @@ export const Task = ({ list }: { list: TaskResult[] }) => {
{!extend && list && (
<div className='fz-[14px] border-b pl-4 font-medium text-[#0969da]'>
{list.map((i) => (
<div
onClick={() => handleClick(i.build_id)}
className='!fz-[14px] flex !h-[37px] items-center gap-2'
key={i.build_id}
>
{identifyStatus(status[i.build_id])}
<span className='cursor-pointer hover:text-[#1f2328]'>{i.mr}</span>
</div>
<TaskItem key={i.build_id} task={i} />
))}
</div>
)}
</>
)
}

export const TaskItem = ({ task }: { task: TaskResult }) => {
const [statusMap] = useAtom(statusMapAtom)

const [_, setBuildId] = useAtom(buildIdAtom)
const handleClick = (build_id: string) => {
// 此处建立连接
// setLoading(true)
setBuildId(build_id)
// if (eventSourcesRef.current[build_id]) return
// setEventSource(build_id)
}

return (
<>
<div
onClick={() => handleClick(task.build_id)}
className='!fz-[14px] flex !h-[37px] items-center gap-2'
key={task.build_id}
>
{identifyStatus(statusMap.get(task.build_id)?.status || Status.NotFound)}
<span className='cursor-pointer hover:text-[#1f2328]'>{task.build_id}</span>
</div>
</>
)
}

export const identifyStatus = (status: Status[keyof Status]) => {
switch (status) {
case Status.Completed:
return <CheckIcon size={14} className='text-[#1a7f37]' />
case Status.Failed:
return <XIcon size={14} className='text-[#d53d46]' />
case Status.Building:
return <LoadingSpinner />
case Status.Pending:
return <LoadingSpinner />
case Status.NotFound:
return <LoadingSpinner />

default:
return <XIcon size={14} className='text-[#d53d46]' />
}
}
17 changes: 12 additions & 5 deletions moon/apps/web/components/MrView/components/Checks/cpns/store.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { atom } from 'jotai'

import { MRTaskStatus } from '@/hooks/SSE/useGetMrTaskStatus'

export enum Status {
Pending = 'pending',
Success = 'success',
Fail = 'fail'
Pending = 'Pending',
Completed = 'Completed',
Failed = 'Failed',
Building = 'Building',
Interrupted = 'Interrupted',
NotFound = 'NotFound'
}

export const logsAtom = atom<Record<string, string[]>>({})
export const logsAtom = atom<Record<string, string>>({})
export const statusAtom = atom<Record<string, Status>>({})
export const loadingAtom = atom(false)
export const loadingAtom = atom(true)
export const statusMapAtom = atom<Map<string, MRTaskStatus>>(new Map())
export const tabAtom = atom<'conversation' | 'check' | 'filechange'>('conversation')
98 changes: 68 additions & 30 deletions moon/apps/web/components/MrView/components/Checks/index.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,67 @@
import { memo, useEffect } from 'react'
import { useEffect } from 'react'
import { LazyLog } from '@melloware/react-logviewer'
import { useAtom } from 'jotai'

import { LoadingSpinner } from '@gitmono/ui/Spinner'

import { buildId } from '@/components/Issues/utils/store'
import { TaskResult, useGetMrTask } from '@/hooks/SSE/useGetMrTask'
import { buildIdAtom } from '@/components/Issues/utils/store'
import { HttpTaskRes } from '@/hooks/SSE/ssmRequest'
// import { TaskResult } from '@/hooks/SSE/useGetMrTask'
import { useGetMrTaskStatus } from '@/hooks/SSE/useGetMrTaskStatus'

import { useTaskSSE } from '../../hook/useSSM'
import { loadingAtom } from './cpns/store'
import { mocks, Task } from './cpns/Task'
import { statusMapAtom } from './cpns/store'
import { Task } from './cpns/Task'

const Checks = ({ mr }: { mr: string }) => {
const { data } = useGetMrTask(mr)
const [buildid, setBuildId] = useAtom(buildId)
const { logsMap, setEventSource } = useTaskSSE()
const [loading] = useAtom(loadingAtom)
// const { data } = useGetMrTask(mr)
const [buildid, setBuildId] = useAtom(buildIdAtom)
const { logsMap, setEventSource, eventSourcesRef, setLogsMap } = useTaskSSE()
const [statusMap, _setStatusMap] = useAtom(statusMapAtom)
// 获取所有构建任务
const { data: status } = useGetMrTaskStatus(mr)

useEffect(() => {
// 构建日志id与日志映射,同时获取已存在日志
if (!status) return
// setBuildId((prev) => prev ?? status[0].build_id)
const fetchLogs = async () => {
const logsResult = await Promise.allSettled(
status.map(async (i) => {
statusMap.set(i.build_id, i)
const res = await HttpTaskRes(i.build_id, 0, 4096)

return res
})
)
const newLogsMap = logsResult.reduce(
(acc, i) => {
if (i.status === 'fulfilled' && i.value) {
acc[i.value.task_id] = i.value.data === '' ? 'empty logs, please check it later' : i.value.data
}
return acc
},
{ ...logsMap }
)

setLogsMap(newLogsMap)
}

fetchLogs()
setBuildId(status[0].build_id)
return () => {
statusMap.clear()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [status])

// 页面加载时建立连接
useEffect(() => {
if (data) {
setBuildId(data[0].build_id)
data.map((i) => setEventSource(i.build_id))
if (status?.length) {
status.map((i) => setEventSource(i.build_id))
}
setBuildId(mocks[0].build_id)
mocks.map((i) => setEventSource(i.build_id))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}, [status])

return (
<>
Expand All @@ -39,32 +74,35 @@ const Checks = ({ mr }: { mr: string }) => {
<div className='flex justify-between' style={{ height: `calc(100vh - 164px)` }}>
{/* left side */}
<div className='h-full w-[40%] border-r'>
{/* {data && <Task list={data} />} */}
<Task list={data as TaskResult[]} />
{status && <Task list={status} />}
{/* <Task list={status as TaskResult[]} /> */}
</div>
{/* right side */}
<div className='flex-1'>
{logsMap[buildid] ? (
<LazyLog
extraLines={1}
text={(logsMap[buildid] ?? []).join('\n')}
stream
enableSearch
caseInsensitive
follow
/>
) : (
loading && (
{
logsMap[buildid] && eventSourcesRef.current[buildid] ? (
<LazyLog extraLines={1} text={logsMap[buildid]} stream enableSearch caseInsensitive follow />
) : eventSourcesRef.current[buildid] ? (
<div></div>
) : (
// <LazyLog extraLines={1} text={logsMap[buildid]} stream enableSearch caseInsensitive follow />
<div className='flex h-full flex-1 items-center justify-center'>
<LoadingSpinner />
</div>
)
)}

// loading && (
// <div className='flex h-full flex-1 items-center justify-center'>
// <LoadingSpinner />
// </div>
// )
}
</div>
</div>
</div>
</>
)
}

export default memo(Checks)
// export default memo(Checks)
export default Checks
20 changes: 15 additions & 5 deletions moon/apps/web/components/MrView/hook/useSSM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,27 @@ export const useTaskSSE = () => {
const [_status, setStatus] = useAtom(statusAtom)

const setEventSource: (taskId: string) => void = (taskId: string) => {
// const es = new EventSource(`/sse/task-output/${taskId}`)
const es = new EventSource(`/api/event?id=${taskId}`)
if (eventSourcesRef.current[taskId]) return
const es = new EventSource(`/sse/task-output/${taskId}`)
// const es = new EventSource(`/api/event?id=${taskId}`)

es.onmessage = (e) => {
setLogsMap((prev) => {
const prevLogs = prev[taskId] ?? []
const prevLogs = prev[taskId] ?? ''
const newLog = e.data + '\n'

// 判断最后一条是否重复
if (prevLogs.endsWith(newLog)) {
return prev // 重复就直接返回原对象
}

return {
...prev,
[taskId]: [...prevLogs, e.data] // 每条消息生成新数组
[taskId]: prevLogs + newLog
}
})

setLoading(false)
}

// status
Expand Down Expand Up @@ -78,11 +87,12 @@ export const useTaskSSE = () => {
Object.values(eventSourcesRef.current).forEach((es) => es.close())
eventSourcesRef.current = {}
setLoading(false)
setLogsMap({})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

return { eventSourcesRef, setEventSource, logsMap }
return { eventSourcesRef, setEventSource, logsMap, setLogsMap }
}

export const useMultiTaskSSE = (taskIds: string[]) => {
Expand Down
Loading