@@ -141,6 +141,11 @@ type State =
141141 , pageCursor :: Maybe PageCursor
142142 }
143143
144+ type FetchResult =
145+ { jobs :: Array Job
146+ , hasNextPage :: Boolean
147+ }
148+
144149-- | Direction of a pagination cursor relative to the sort order.
145150data PaginationDir = Forward | Backward
146151
@@ -168,7 +173,7 @@ data Action
168173 = Initialize
169174 | FetchJobs
170175 | FetchJobsSilent
171- | HandleFetchResult (Either ApiError ( Array Job ) )
176+ | HandleFetchResult (Either ApiError FetchResult )
172177 | SetTimeRange TimeRange
173178 | SetCustomSinceDate String
174179 | SetCustomSinceTime String
@@ -687,21 +692,16 @@ handleAction = case _ of
687692 else
688693 msg
689694 H .modify_ _ { loading = false , error = Just displayMsg, jobs = [] }
690- Right jobs -> do
695+ Right { jobs, hasNextPage } -> do
691696 state <- H .get
692697 let summaries = map Job .toJobSummary jobs
693698 let newFingerprints = map jobFingerprint summaries
694699 let oldFingerprints = map jobFingerprint state.jobs
695700 -- Skip the state update when the set of jobs hasn't changed. This
696701 -- avoids VDOM diffing on every auto-refresh tick when nothing new
697702 -- has arrived.
698- unless (not state.loading && newFingerprints == oldFingerprints) do
699- let
700- isBackward = case state.pageCursor of
701- Just { dir: Backward } -> true
702- _ -> false
703- let hasNext = isBackward || Array .length jobs >= pageSize
704- H .modify_ _ { loading = false , error = Nothing , jobs = summaries, hasNextPage = hasNext }
703+ unless (not state.loading && newFingerprints == oldFingerprints && state.hasNextPage == hasNextPage) do
704+ H .modify_ _ { loading = false , error = Nothing , jobs = summaries, hasNextPage = hasNextPage }
705705
706706 SetTimeRange range -> do
707707 when (range == Custom ) do
@@ -773,11 +773,7 @@ handleAction = case _ of
773773 NextPage -> do
774774 state <- H .get
775775 when (state.hasNextPage && not state.loading) do
776- let
777- cursor = case state.sortOrder of
778- DESC -> extremeCreatedAt min state.jobs
779- ASC -> extremeCreatedAt max state.jobs
780- case cursor of
776+ case nextPageCursor state.sortOrder state.jobs of
781777 Nothing -> pure unit
782778 Just ts -> do
783779 H .modify_ _ { currentPage = state.currentPage + 1 , pageCursor = Just { timestamp: ts, dir: Forward } }
@@ -794,11 +790,7 @@ handleAction = case _ of
794790 handleAction FetchJobs
795791 notifyFiltersChanged
796792 else do
797- let
798- cursor = case state.sortOrder of
799- DESC -> extremeCreatedAt max state.jobs
800- ASC -> extremeCreatedAt min state.jobs
801- case cursor of
793+ case prevPageCursor state.sortOrder state.jobs of
802794 Nothing -> pure unit
803795 Just ts -> do
804796 H .modify_ _ { currentPage = targetPage, pageCursor = Just { timestamp: ts, dir: Backward }, hasNextPage = true }
@@ -827,10 +819,27 @@ jobFingerprint job =
827819 }
828820
829821-- | Build query parameters from the current state and fetch jobs from the API.
830- doFetchJobs :: forall m . MonadAff m => H.HalogenM State Action () Output m (Either ApiError ( Array Job ) )
822+ doFetchJobs :: forall m . MonadAff m => H.HalogenM State Action () Output m (Either ApiError FetchResult )
831823doFetchJobs = do
832824 state <- H .get
833825 now <- liftEffect Now .nowDateTime
826+ jobsResult <- fetchJobsPage state now state.pageCursor
827+ case jobsResult of
828+ Left err ->
829+ pure (Left err)
830+ Right jobs -> do
831+ hasNextPage <- determineHasNextPage state now jobs
832+ pure (Right { jobs, hasNextPage })
833+
834+ -- | Fetch one jobs page using the current filters and an optional pagination cursor.
835+ fetchJobsPage
836+ :: forall m
837+ . MonadAff m
838+ => State
839+ -> DateTime
840+ -> Maybe PageCursor
841+ -> H.HalogenM State Action () Output m (Either ApiError (Array Job ))
842+ fetchJobsPage state now cursor = do
834843 let
835844 customSince = Job .parseDateTimeLocal state.sinceStr
836845 customUntil = Job .parseDateTimeLocal state.untilStr
@@ -842,7 +851,7 @@ doFetchJobs = do
842851 Custom -> customUntil
843852 UntilNow -> Just now
844853 _ -> Nothing
845- { since, until, fetchOrder, needsReverse } = case state.pageCursor of
854+ { since, until, fetchOrder, needsReverse } = case cursor of
846855 Nothing ->
847856 { since: baseSince, until: baseUntil, fetchOrder: state.sortOrder, needsReverse: false }
848857 Just { timestamp, dir: Forward } -> case state.sortOrder of
@@ -853,7 +862,34 @@ doFetchJobs = do
853862 ASC -> { since: baseSince, until: Just timestamp, fetchOrder: DESC , needsReverse: true }
854863 includeCompleted = Just (state.filters.statusFilter /= ActiveOnly )
855864 result <- H .liftAff $ API .fetchJobs state.apiConfig { since, until, order: Just fetchOrder, includeCompleted }
856- pure $ if needsReverse then map Array .reverse result else result
865+ pure $ map (normalizeFetchedJobs state.sortOrder cursor <<< applyFetchedOrder needsReverse) result
866+
867+ -- | Decide whether another page exists past the current page. Full pages require
868+ -- | a probe request because the jobs API is capped at `pageSize`.
869+ determineHasNextPage
870+ :: forall m
871+ . MonadAff m
872+ => State
873+ -> DateTime
874+ -> Array Job
875+ -> H.HalogenM State Action () Output m Boolean
876+ determineHasNextPage state now jobs = do
877+ let
878+ isBackward = case state.pageCursor of
879+ Just { dir: Backward } -> true
880+ _ -> false
881+ if isBackward then
882+ pure true
883+ else if Array .length jobs < pageSize then
884+ pure false
885+ else case nextPageCursorForJobs state.sortOrder jobs of
886+ Nothing ->
887+ pure false
888+ Just ts -> do
889+ probe <- fetchJobsPage state now (Just { timestamp: ts, dir: Forward })
890+ pure case probe of
891+ Left _ -> true
892+ Right nextJobs -> not (Array .null nextJobs)
857893
858894-- | Update the combined sinceStr from a date or time part change, fetch if
859895-- | both endpoints parse, and sync the URL.
@@ -897,14 +933,60 @@ syncAutoRefresh enabled = do
897933refreshInterval :: Milliseconds
898934refreshInterval = Milliseconds 5000.0
899935
900- -- | Find the extreme (min or max) `createdAt` timestamp among job summaries .
901- extremeCreatedAt :: ( DateTime -> DateTime -> DateTime ) -> Array JobSummary -> Maybe DateTime
902- extremeCreatedAt pick = Array .foldl (\acc s -> Just (maybe s.createdAt (pick s.createdAt ) acc)) Nothing
936+ -- | Find the extreme (min or max) `createdAt` timestamp using the provided accessor .
937+ extremeCreatedAtBy :: forall a . ( a -> DateTime ) -> ( DateTime -> DateTime -> DateTime ) -> Array a -> Maybe DateTime
938+ extremeCreatedAtBy getCreatedAt pick = Array .foldl (\acc row -> Just (maybe (getCreatedAt row) (pick (getCreatedAt row) ) acc)) Nothing
903939 where
904940 maybe fallback f = case _ of
905941 Nothing -> fallback
906942 Just x -> f x
907943
944+ nextPageCursor :: forall a . SortOrder -> Array { createdAt :: DateTime | a } -> Maybe DateTime
945+ nextPageCursor sortOrder jobs = case sortOrder of
946+ DESC -> extremeCreatedAtBy _.createdAt min jobs
947+ ASC -> extremeCreatedAtBy _.createdAt max jobs
948+
949+ prevPageCursor :: forall a . SortOrder -> Array { createdAt :: DateTime | a } -> Maybe DateTime
950+ prevPageCursor sortOrder jobs = case sortOrder of
951+ DESC -> extremeCreatedAtBy _.createdAt max jobs
952+ ASC -> extremeCreatedAtBy _.createdAt min jobs
953+
954+ nextPageCursorForJobs :: SortOrder -> Array Job -> Maybe DateTime
955+ nextPageCursorForJobs sortOrder jobs = case sortOrder of
956+ DESC -> extremeCreatedAtBy (V1 .jobInfo >>> _.createdAt) min jobs
957+ ASC -> extremeCreatedAtBy (V1 .jobInfo >>> _.createdAt) max jobs
958+
959+ applyFetchedOrder :: Boolean -> Array Job -> Array Job
960+ applyFetchedOrder needsReverse =
961+ if needsReverse then Array .reverse else identity
962+
963+ normalizeFetchedJobs :: SortOrder -> Maybe PageCursor -> Array Job -> Array Job
964+ normalizeFetchedJobs sortOrder cursor jobs = case cursor of
965+ Just { timestamp, dir: Forward }
966+ | sortOrder == ASC ->
967+ dropBoundaryFromStart timestamp jobs
968+ Just { timestamp, dir: Backward }
969+ | sortOrder == DESC ->
970+ dropBoundaryFromEnd timestamp jobs
971+ _ ->
972+ jobs
973+
974+ dropBoundaryFromStart :: DateTime -> Array Job -> Array Job
975+ dropBoundaryFromStart timestamp jobs = case Array .uncons jobs of
976+ Just { head, tail }
977+ | (V1 .jobInfo head).createdAt == timestamp ->
978+ tail
979+ _ ->
980+ jobs
981+
982+ dropBoundaryFromEnd :: DateTime -> Array Job -> Array Job
983+ dropBoundaryFromEnd timestamp jobs = case Array .unsnoc jobs of
984+ Just { init, last }
985+ | (V1 .jobInfo last).createdAt == timestamp ->
986+ init
987+ _ ->
988+ jobs
989+
908990-- | Apply client-side filters to an array of job summaries.
909991applyFilters :: Filters -> Array JobSummary -> Array JobSummary
910992applyFilters filters = Array .filter matchesAll
0 commit comments