replication-pipeline-request-status.tsx 4.5 KB

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