Skip to content

Commit 3567c89

Browse files
committed
Few pagination fixes
1 parent 848483e commit 3567c89

5 files changed

Lines changed: 123 additions & 45 deletions

File tree

dashboard/src/Dashboard/Component/JobDetail.purs

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import Data.Newtype (unwrap)
2222
import Effect.Aff (Milliseconds(..))
2323
import Effect.Aff.Class (class MonadAff)
2424
import Effect.Class (liftEffect)
25+
import Effect.Now as Now
2526
import Halogen as H
2627
import Halogen.HTML as HH
2728
import Halogen.HTML.Events as HE
@@ -67,6 +68,7 @@ type State =
6768
, payloadCollapsed :: Boolean
6869
, logUntil :: Maybe DateTime
6970
, logPage :: Int
71+
, currentTime :: Maybe DateTime
7072
}
7173

7274
data Action
@@ -117,6 +119,7 @@ initialState input =
117119
, payloadCollapsed: false
118120
, logUntil: Nothing
119121
, logPage: 0
122+
, currentTime: Nothing
120123
}
121124

122125
-- --------------------------------------------------------------------------
@@ -189,15 +192,15 @@ renderJobDetail state job = do
189192
let info = V1.jobInfo job
190193
let statusName = Job.printStatus (Job.deriveStatus info)
191194
HH.div [ HP.class_ (HH.ClassName "job-detail") ]
192-
[ renderInfoBlock info statusName (Job.getCompilerVersion job)
195+
[ renderInfoBlock state.currentTime info statusName (Job.getCompilerVersion job)
193196
, renderPayloadSection state job
194197
, renderLogsSection state
195198
]
196199

197-
renderInfoBlock :: forall m. V1.JobInfo () -> String -> Maybe Version -> H.ComponentHTML Action () m
198-
renderInfoBlock info statusName compiler = do
199-
let waitDuration = computeDurationBetween info.createdAt info.startedAt
200-
let runDuration = map (\s -> computeDurationBetween s info.finishedAt) info.startedAt
200+
renderInfoBlock :: forall m. Maybe DateTime -> V1.JobInfo () -> String -> Maybe Version -> H.ComponentHTML Action () m
201+
renderInfoBlock mNow info statusName compiler = do
202+
let waitDuration = computeDurationBetween mNow info.createdAt info.startedAt
203+
let runDuration = map (\s -> computeDurationBetween mNow s info.finishedAt) info.startedAt
201204
HH.div [ HP.class_ (HH.ClassName "job-detail__timestamps") ]
202205
( Array.catMaybes
203206
[ Just $ renderInfoRow "Job ID"
@@ -318,17 +321,20 @@ renderLogEntries state
318321
, HH.th [ HP.class_ (HH.ClassName "log-table__th") ] [ HH.text "Message" ]
319322
]
320323
]
321-
, HH.tbody_ (Array.mapWithIndex (renderLogEntry pageStart) pageLogs)
324+
, HH.tbody_ (Array.mapWithIndex (renderLogEntry state.logSortOrder totalLogs pageStart) pageLogs)
322325
]
323326
, renderLogPagination page totalPages totalLogs
324327
]
325328

326-
renderLogEntry :: forall m. Int -> Int -> LogLine -> H.ComponentHTML Action () m
327-
renderLogEntry offset index logLine = do
329+
renderLogEntry :: forall m. SortOrder -> Int -> Int -> Int -> LogLine -> H.ComponentHTML Action () m
330+
renderLogEntry sortOrder totalLogs offset index logLine = do
328331
let level = V1.printLogLevel logLine.level
332+
let rowNum = case sortOrder of
333+
ASC -> offset + index + 1
334+
DESC -> totalLogs - offset - index
329335
HH.tr [ HP.class_ (HH.ClassName ("log-entry log-entry--" <> level)) ]
330336
[ HH.td [ HP.class_ (HH.ClassName "log-entry__rownum") ]
331-
[ HH.text (show (offset + index + 1)) ]
337+
[ HH.text (show rowNum) ]
332338
, HH.td [ HP.class_ (HH.ClassName "log-entry__time") ]
333339
[ HH.text (Job.formatTimestamp logLine.timestamp) ]
334340
, HH.td [ HP.class_ (HH.ClassName "log-entry__level") ]
@@ -370,6 +376,8 @@ renderLogPagination page totalPages totalLogs
370376
handleAction :: forall m. MonadAff m => Action -> H.HalogenM State Action () Output m Unit
371377
handleAction = case _ of
372378
Initialize -> do
379+
now <- liftEffect Now.nowDateTime
380+
H.modify_ _ { currentTime = Just now }
373381
handleAction FetchJob
374382
state <- H.get
375383
let finished = case state.job of
@@ -397,6 +405,7 @@ handleAction = case _ of
397405
let msg = API.printApiError err
398406
H.modify_ _ { loading = false, error = Just msg, job = Nothing }
399407
Right job -> do
408+
now <- liftEffect Now.nowDateTime
400409
let info = V1.jobInfo job
401410
let logs = info.logs
402411
let lastTs = map _.timestamp (Array.last logs)
@@ -408,6 +417,7 @@ handleAction = case _ of
408417
, lastLogTimestamp = lastTs
409418
, logUntil = info.finishedAt
410419
, logPage = 0
420+
, currentTime = Just now
411421
}
412422
fetchAllRemainingLogs
413423
stopAutoRefreshIfFinished info
@@ -445,6 +455,8 @@ handleAction = case _ of
445455
}
446456

447457
LogRefreshTick -> do
458+
now <- liftEffect Now.nowDateTime
459+
H.modify_ _ { currentTime = Just now }
448460
state <- H.get
449461
-- No-op if the job is already finished: all logs have been fetched.
450462
let finished = case state.job of
@@ -460,6 +472,7 @@ handleAction = case _ of
460472
-- next tick will attempt another fetch automatically.
461473
Left _ -> pure unit
462474
Right job -> do
475+
now <- liftEffect Now.nowDateTime
463476
state <- H.get
464477
let info = V1.jobInfo job
465478
let newLogs = Array.filter (isNewerThan state.lastLogTimestamp) info.logs
@@ -469,7 +482,7 @@ handleAction = case _ of
469482
let combined = capLogs state.logSortOrder (state.allLogs <> newLogs)
470483
H.modify_ _ { allLogs = combined, lastLogTimestamp = lastTs }
471484
-- Update job status and logUntil from the refreshed data
472-
H.modify_ _ { job = Just job, logUntil = info.finishedAt }
485+
H.modify_ _ { job = Just job, logUntil = info.finishedAt, currentTime = Just now }
473486
stopAutoRefreshIfFinished info
474487

475488
TogglePayload ->
@@ -552,11 +565,14 @@ getPayloadJson = case _ of
552565
PackageSetJob j -> JSON.printIndented (CJ.encode Operation.packageSetOperationCodec j.payload)
553566

554567
-- | Compute a human-readable duration between a start time and an optional
555-
-- | end time. If the end time is absent, shows "ongoing".
556-
computeDurationBetween :: DateTime -> Maybe DateTime -> String
557-
computeDurationBetween start = case _ of
558-
Nothing -> "ongoing"
568+
-- | end time. If the end time is absent, uses the current time as a fallback
569+
-- | and appends "(ongoing)".
570+
computeDurationBetween :: Maybe DateTime -> DateTime -> Maybe DateTime -> String
571+
computeDurationBetween mNow start = case _ of
559572
Just end -> Job.formatDurationBetween start end
573+
Nothing -> case mNow of
574+
Just now -> Job.formatDurationBetween start now <> " (ongoing)"
575+
Nothing -> "ongoing"
560576

561577
-- | Fetch all remaining log pages after the initial fetch by looping until
562578
-- | either an empty batch is returned or the maximum number of iterations

dashboard/src/Dashboard/Component/JobsList.purs

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,32 @@ type State =
137137
, currentPage :: Int
138138
-- | Whether there are more results beyond the current page.
139139
, hasNextPage :: Boolean
140-
-- | Boundary timestamps for pages we have visited. `pageCursors !! 0` is
141-
-- | the cursor that takes us from page 1 to page 2, etc.
142-
, pageCursors :: Array DateTime
140+
-- | The cursor used to fetch the current page, if any.
141+
, pageCursor :: Maybe PageCursor
143142
}
144143

144+
-- | Direction of a pagination cursor relative to the sort order.
145+
data PaginationDir = Forward | Backward
146+
147+
derive instance Eq PaginationDir
148+
149+
-- | A pagination cursor: a boundary timestamp and the direction it was
150+
-- | computed from (forward = NextPage, backward = PrevPage).
151+
type PageCursor = { timestamp :: DateTime, dir :: PaginationDir }
152+
153+
printCursorParam :: PageCursor -> String
154+
printCursorParam { timestamp, dir } =
155+
(case dir of
156+
Forward -> "f:"
157+
Backward -> "b:")
158+
<> Job.formatCursorTimestamp timestamp
159+
160+
parseCursorParam :: String -> Maybe PageCursor
161+
parseCursorParam s = case String.take 2 s of
162+
"f:" -> map (\dt -> { timestamp: dt, dir: Forward }) (Job.parseCursorTimestamp (String.drop 2 s))
163+
"b:" -> map (\dt -> { timestamp: dt, dir: Backward }) (Job.parseCursorTimestamp (String.drop 2 s))
164+
_ -> Nothing
165+
145166
data Action
146167
= Initialize
147168
| FetchJobs
@@ -205,7 +226,9 @@ initialState input = do
205226
, untilStr: fromMaybe "" p.until
206227
, currentPage: fromMaybe 1 p.page
207228
, hasNextPage: true
208-
, pageCursors: []
229+
, pageCursor: case p.page, p.cursor >>= parseCursorParam of
230+
Just pg, Just pc | pg > 1 -> Just pc
231+
_, _ -> Nothing
209232
}
210233

211234
-- --------------------------------------------------------------------------
@@ -246,6 +269,9 @@ stateToParams s =
246269
, page:
247270
if s.currentPage <= 1 then Nothing
248271
else Just s.currentPage
272+
, cursor: case s.pageCursor of
273+
Just pc | s.currentPage > 1 -> Just (printCursorParam pc)
274+
_ -> Nothing
249275
}
250276

251277
-- --------------------------------------------------------------------------
@@ -630,7 +656,9 @@ handleAction = case _ of
630656
, currentPage = fromMaybe 1 p.page
631657
, since = Nothing
632658
, until = Nothing
633-
, pageCursors = []
659+
, pageCursor = case p.page, p.cursor >>= parseCursorParam of
660+
Just pg, Just pc | pg > 1 -> Just pc
661+
_, _ -> Nothing
634662
, hasNextPage = true
635663
}
636664
handleAction FetchJobs
@@ -663,15 +691,18 @@ handleAction = case _ of
663691
-- avoids VDOM diffing on every auto-refresh tick when nothing new
664692
-- has arrived.
665693
unless (not state.loading && newFingerprints == oldFingerprints) do
666-
let hasNext = Array.length jobs >= pageSize
694+
let isBackward = case state.pageCursor of
695+
Just { dir: Backward } -> true
696+
_ -> false
697+
let hasNext = isBackward || Array.length jobs >= pageSize
667698
H.modify_ _ { loading = false, error = Nothing, jobs = summaries, hasNextPage = hasNext }
668699

669700
SetTimeRange range -> do
670701
when (range == Custom) do
671702
now <- liftEffect Now.nowDateTime
672703
let sinceDefault = subtractHours 24.0 now
673704
H.modify_ _ { sinceStr = Job.formatDateTimeLocal sinceDefault, untilStr = Job.formatDateTimeLocal now }
674-
H.modify_ _ { timeRange = range, since = Nothing, until = Nothing, currentPage = 1, pageCursors = [], hasNextPage = true }
705+
H.modify_ _ { timeRange = range, since = Nothing, until = Nothing, currentPage = 1, pageCursor = Nothing, hasNextPage = true }
675706
handleAction FetchJobs
676707
notifyFiltersChanged
677708

@@ -722,17 +753,17 @@ handleAction = case _ of
722753
-- Re-fetch when switching between Active and other modes, because
723754
-- Active excludes completed jobs server-side.
724755
when (needsRefetch state) do
725-
H.modify_ _ { currentPage = 1, pageCursors = [], hasNextPage = true }
756+
H.modify_ _ { currentPage = 1, pageCursor = Nothing, hasNextPage = true }
726757
handleAction FetchJobs
727758

728759
ClearFilters -> do
729-
H.modify_ _ { filters = emptyFilters, sortOrder = defaultSortOrder, currentPage = 1, pageCursors = [], hasNextPage = true }
760+
H.modify_ _ { filters = emptyFilters, sortOrder = defaultSortOrder, currentPage = 1, pageCursor = Nothing, hasNextPage = true }
730761
notifyFiltersChanged
731762

732763
SetSort field -> do
733764
H.modify_ \s -> do
734765
let newOrder = if s.sortField == field then (if s.sortOrder == DESC then ASC else DESC) else DESC
735-
s { sortField = field, sortOrder = newOrder, currentPage = 1, pageCursors = [], hasNextPage = true }
766+
s { sortField = field, sortOrder = newOrder, currentPage = 1, pageCursor = Nothing, hasNextPage = true }
736767
handleAction FetchJobs
737768
notifyFiltersChanged
738769

@@ -749,18 +780,30 @@ handleAction = case _ of
749780
case cursor of
750781
Nothing -> pure unit
751782
Just ts -> do
752-
let newCursors = state.pageCursors <> [ ts ]
753-
H.modify_ _ { currentPage = state.currentPage + 1, pageCursors = newCursors }
783+
H.modify_ _ { currentPage = state.currentPage + 1, pageCursor = Just { timestamp: ts, dir: Forward } }
754784
handleAction FetchJobs
755785
notifyFiltersChanged
756786

757787
PrevPage -> do
758788
state <- H.get
759789
when (state.currentPage > 1) do
760-
let newCursors = fromMaybe [] (Array.init state.pageCursors)
761-
H.modify_ _ { currentPage = state.currentPage - 1, pageCursors = newCursors, hasNextPage = true }
762-
handleAction FetchJobs
763-
notifyFiltersChanged
790+
let targetPage = state.currentPage - 1
791+
if targetPage <= 1 then do
792+
-- Arriving at page 1: reset cursor for fresh data
793+
H.modify_ _ { currentPage = 1, pageCursor = Nothing, hasNextPage = true }
794+
handleAction FetchJobs
795+
notifyFiltersChanged
796+
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
802+
Nothing -> pure unit
803+
Just ts -> do
804+
H.modify_ _ { currentPage = targetPage, pageCursor = Just { timestamp: ts, dir: Backward }, hasNextPage = true }
805+
handleAction FetchJobs
806+
notifyFiltersChanged
764807

765808
Tick ->
766809
handleAction FetchJobsSilent
@@ -799,25 +842,24 @@ doFetchJobs = do
799842
Custom -> customUntil
800843
UntilNow -> Just now
801844
_ -> Nothing
802-
pageCursor = Array.index state.pageCursors (state.currentPage - 2)
803-
since = case state.sortOrder of
804-
DESC -> baseSince
805-
ASC ->
806-
if state.currentPage > 1 then pageCursor
807-
else baseSince
808-
until = case state.sortOrder of
809-
DESC ->
810-
if state.currentPage > 1 then pageCursor
811-
else baseUntil
812-
ASC -> baseUntil
845+
{ since, until, fetchOrder, needsReverse } = case state.pageCursor of
846+
Nothing ->
847+
{ since: baseSince, until: baseUntil, fetchOrder: state.sortOrder, needsReverse: false }
848+
Just { timestamp, dir: Forward } -> case state.sortOrder of
849+
DESC -> { since: baseSince, until: Just timestamp, fetchOrder: DESC, needsReverse: false }
850+
ASC -> { since: Just timestamp, until: baseUntil, fetchOrder: ASC, needsReverse: false }
851+
Just { timestamp, dir: Backward } -> case state.sortOrder of
852+
DESC -> { since: Just timestamp, until: baseUntil, fetchOrder: ASC, needsReverse: true }
853+
ASC -> { since: baseSince, until: Just timestamp, fetchOrder: DESC, needsReverse: true }
813854
includeCompleted = Just (state.filters.statusFilter /= ActiveOnly)
814-
H.liftAff $ API.fetchJobs state.apiConfig { since, until, order: Just state.sortOrder, includeCompleted }
855+
result <- H.liftAff $ API.fetchJobs state.apiConfig { since, until, order: Just fetchOrder, includeCompleted }
856+
pure $ if needsReverse then map Array.reverse result else result
815857

816858
-- | Update the combined sinceStr from a date or time part change, fetch if
817859
-- | both endpoints parse, and sync the URL.
818860
updateCustomSince :: forall m. MonadAff m => String -> H.HalogenM State Action () Output m Unit
819861
updateCustomSince newSince = do
820-
H.modify_ _ { sinceStr = newSince, since = Nothing, until = Nothing, currentPage = 1, pageCursors = [], hasNextPage = true }
862+
H.modify_ _ { sinceStr = newSince, since = Nothing, until = Nothing, currentPage = 1, pageCursor = Nothing, hasNextPage = true }
821863
state <- H.get
822864
case Job.parseDateTimeLocal newSince, Job.parseDateTimeLocal state.untilStr of
823865
Just _, Just _ -> handleAction FetchJobs
@@ -828,7 +870,7 @@ updateCustomSince newSince = do
828870
-- | both endpoints parse, and sync the URL.
829871
updateCustomUntil :: forall m. MonadAff m => String -> H.HalogenM State Action () Output m Unit
830872
updateCustomUntil newUntil = do
831-
H.modify_ _ { untilStr = newUntil, since = Nothing, until = Nothing, currentPage = 1, pageCursors = [], hasNextPage = true }
873+
H.modify_ _ { untilStr = newUntil, since = Nothing, until = Nothing, currentPage = 1, pageCursor = Nothing, hasNextPage = true }
832874
state <- H.get
833875
case Job.parseDateTimeLocal state.sinceStr, Job.parseDateTimeLocal newUntil of
834876
Just _, Just _ -> handleAction FetchJobs

dashboard/src/Dashboard/Job.purs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ module Dashboard.Job
1414
, formatTimestamp
1515
, formatDateTimeLocal
1616
, parseDateTimeLocal
17+
, formatCursorTimestamp
18+
, parseCursorTimestamp
1719
, formatDurationSecs
1820
, formatDurationBetween
1921
, timerEmitter
@@ -159,6 +161,21 @@ dateTimeLocalFormat = List.fromFoldable
159161
, Placeholder "T", Hours24, Placeholder ":", MinutesTwoDigits
160162
]
161163

164+
-- | Format a DateTime as a URL-safe cursor timestamp "YYYY-MM-DDTHH:MM:SS".
165+
formatCursorTimestamp :: DateTime -> String
166+
formatCursorTimestamp = Formatter.DateTime.format cursorTimestampFormat
167+
168+
-- | Parse a cursor timestamp ("YYYY-MM-DDTHH:MM:SS") into a DateTime.
169+
parseCursorTimestamp :: String -> Maybe DateTime
170+
parseCursorTimestamp = hush <<< Formatter.DateTime.unformat cursorTimestampFormat
171+
172+
-- "YYYY-MM-DDTHH:MM:SS"
173+
cursorTimestampFormat :: List FormatterCommand
174+
cursorTimestampFormat = List.fromFoldable
175+
[ YearFull, Placeholder "-", MonthTwoDigits, Placeholder "-", DayOfMonthTwoDigits
176+
, Placeholder "T", Hours24, Placeholder ":", MinutesTwoDigits, Placeholder ":", SecondsTwoDigits
177+
]
178+
162179
-- | Format a duration in seconds as a human-readable string.
163180
formatDurationSecs :: Int -> String
164181
formatDurationSecs totalSecs

dashboard/src/Dashboard/Route.purs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type JobsListParams =
2828
, until :: Maybe String
2929
, order :: Maybe String
3030
, page :: Maybe Int
31+
, cursor :: Maybe String
3132
}
3233

3334
-- | All-Nothing params representing defaults (no filters active).
@@ -44,6 +45,7 @@ defaultParams =
4445
, until: Nothing
4546
, order: Nothing
4647
, page: Nothing
48+
, cursor: Nothing
4749
}
4850

4951
data Route
@@ -91,4 +93,5 @@ routes = RD.root routeChoice
9193
, until: RD.optional <<< RD.string
9294
, order: RD.optional <<< RD.string
9395
, page: RD.optional <<< RD.int <<< RD.string
96+
, cursor: RD.optional <<< RD.string
9497
}

dashboard/src/Dashboard/Router.purs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ handleAction = case _ of
146146
liftEffect $ setHash ("#/jobs/" <> jobId)
147147

148148
HandleJobsListOutput (JobsList.FiltersChanged params) -> do
149-
H.modify_ _ { lastJobsListParams = params }
149+
H.modify_ _ { route = JobsList params, lastJobsListParams = params }
150150
liftEffect $ replaceHash (routeToHash (JobsList params))
151151

152152
HandleJobDetailOutput JobDetail.NavigateBack -> do

0 commit comments

Comments
 (0)