replication-pipeline-request-status.tsx146 lines · main
1import {
2 createContext,
3 ReactNode,
4 useCallback,
5 useContext,
6 useEffect,
7 useRef,
8 useState,
9} from 'react'
10
11export enum PipelineStatusRequestStatus {
12 None = 'None',
13 StartRequested = 'StartRequested',
14 StopRequested = 'StopRequested',
15 RestartRequested = 'RestartRequested',
16}
17
18interface PipelineRequestStatusContextType {
19 requestStatus: Record<number, PipelineStatusRequestStatus>
20 pipelineStatusSnapshot: Record<number, string | undefined>
21 setRequestStatus: (
22 pipelineId: number,
23 status: PipelineStatusRequestStatus,
24 snapshotStatus?: string
25 ) => void
26 getRequestStatus: (pipelineId: number) => PipelineStatusRequestStatus
27 updatePipelineStatus: (pipelineId: number, backendStatus: string | undefined) => void
28}
29
30interface PipelineRequestStatusProviderProps {
31 children: ReactNode
32}
33
34const PipelineRequestStatusContext = createContext<PipelineRequestStatusContextType | undefined>(
35 undefined
36)
37
38// [Joshen] Leaving a comment for future investigation
39// Do we need this? Afaict the status is getting returned from the API so we might not need
40// to track the pipeline status on the client side
41export const PipelineRequestStatusProvider = ({ children }: PipelineRequestStatusProviderProps) => {
42 const [requestStatus, setRequestStatusState] = useState<
43 Record<number, PipelineStatusRequestStatus>
44 >({})
45 const [pipelineStatusSnapshot, setPipelineStatusSnapshot] = useState<
46 Record<number, string | undefined>
47 >({})
48 const timeoutsRef = useRef<Record<number, number>>({})
49 const REQUEST_TIMEOUT_MS = 10_000
50
51 const setRequestStatus = (
52 pipelineId: number,
53 status: PipelineStatusRequestStatus,
54 snapshotStatus?: string
55 ) => {
56 setRequestStatusState((prev) => ({
57 ...prev,
58 [pipelineId]: status,
59 }))
60 setPipelineStatusSnapshot((prev) => {
61 if (status === PipelineStatusRequestStatus.None) {
62 const { [pipelineId]: _omit, ...rest } = prev
63 return rest
64 }
65 // Only set snapshot when provided to avoid undefined entries
66 if (snapshotStatus !== undefined) {
67 return { ...prev, [pipelineId]: snapshotStatus }
68 }
69 return prev
70 })
71
72 // Clear existing timeout for this pipeline
73 const existing = timeoutsRef.current[pipelineId]
74 if (existing !== undefined) {
75 clearTimeout(existing)
76 delete timeoutsRef.current[pipelineId]
77 }
78
79 // Start auto-reset timer for non-None states
80 if (status !== PipelineStatusRequestStatus.None) {
81 const id = window.setTimeout(() => {
82 // If still pending, clear to None to show backend state
83 setRequestStatusState((prev) => {
84 if (prev[pipelineId] && prev[pipelineId] !== PipelineStatusRequestStatus.None) {
85 return { ...prev, [pipelineId]: PipelineStatusRequestStatus.None }
86 }
87 return prev
88 })
89 setPipelineStatusSnapshot((prev) => {
90 const { [pipelineId]: _omit, ...rest } = prev
91 return rest
92 })
93 delete timeoutsRef.current[pipelineId]
94 }, REQUEST_TIMEOUT_MS)
95 timeoutsRef.current[pipelineId] = id
96 }
97 }
98
99 const getRequestStatus = (pipelineId: number): PipelineStatusRequestStatus => {
100 return requestStatus[pipelineId] || PipelineStatusRequestStatus.None
101 }
102
103 const updatePipelineStatus = useCallback(
104 (pipelineId: number, newStatus: string | undefined) => {
105 const currentRequestStatus = requestStatus[pipelineId] || PipelineStatusRequestStatus.None
106 if (currentRequestStatus === PipelineStatusRequestStatus.None) return
107
108 // Only remove when backend status differs from snapshot
109 const snapshotStatus = pipelineStatusSnapshot[pipelineId]
110 if (newStatus !== snapshotStatus) {
111 setRequestStatus(pipelineId, PipelineStatusRequestStatus.None)
112 }
113 },
114 [requestStatus, pipelineStatusSnapshot]
115 )
116
117 // Cleanup all timers on unmount
118 useEffect(() => {
119 return () => {
120 Object.values(timeoutsRef.current).forEach((id) => clearTimeout(id))
121 timeoutsRef.current = {}
122 }
123 }, [])
124
125 return (
126 <PipelineRequestStatusContext.Provider
127 value={{
128 requestStatus,
129 pipelineStatusSnapshot,
130 setRequestStatus,
131 getRequestStatus,
132 updatePipelineStatus,
133 }}
134 >
135 {children}
136 </PipelineRequestStatusContext.Provider>
137 )
138}
139
140export const usePipelineRequestStatus = () => {
141 const context = useContext(PipelineRequestStatusContext)
142 if (context === undefined) {
143 throw new Error('usePipelineRequestStatus must be used within a PipelineRequestStatusProvider')
144 }
145 return context
146}