Skip to content

Commit 1654d24

Browse files
authored
Add tabbar to 'Recent Activity' (#266)
Added tab-based filtering for Recent Activity page to show built vs unbuilt branches separately, in addition to all activity. Resolves #264 --- <img width="1207" height="668" alt="image" src="https://github.com/user-attachments/assets/e752a733-b9de-4a7c-ab92-a744fa591ac7" />
1 parent 02e8b52 commit 1654d24

File tree

14 files changed

+280
-89
lines changed

14 files changed

+280
-89
lines changed

packages/vira/src/Vira/CI/AutoBuild.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,4 @@ handleBranchUpdates autoBuildNewBranches repo updates = do
116116
skipReasonText :: SkipReason -> Text
117117
skipReasonText = \case
118118
OldCommit -> "old commit"
119-
NeverBuilt -> "never build before (autoBuildNewBranches=False)"
119+
NeverBuilt -> "never built before (autoBuildNewBranches=False)"

packages/vira/src/Vira/State/Acid.hs

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -75,41 +75,56 @@ getRepoByNameA name = do
7575
enrichBranchWithJobs :: IxJob -> Branch -> BranchDetails
7676
enrichBranchWithJobs jobsIx branch =
7777
let branchJobs = Ix.toDescList (Proxy @JobId) $ jobsIx @= branch.repoName @= branch.branchName
78-
mLatestJob = viaNonEmpty head branchJobs
79-
-- Compute badge state based on job and commit comparison
80-
badgeState = case mLatestJob of
81-
Nothing -> Just NeverBuilt
78+
-- Compute build state based on job and commit comparison
79+
buildState = case viaNonEmpty head branchJobs of
80+
Nothing -> NeverBuilt
8281
Just job
83-
| job.commit /= branch.headCommit.id -> Just OutOfDate
84-
| otherwise -> Nothing
82+
| job.commit /= branch.headCommit.id -> Built job OutOfDate
83+
| otherwise -> Built job UpToDate
8584
in BranchDetails
8685
{ branch
87-
, mLatestJob
8886
, jobsCount = fromIntegral $ length branchJobs
89-
, badgeState
87+
, buildState
9088
}
9189

92-
{- | Get branches with enriched metadata, optionally filtered by repo and/or name.
90+
{- | Query branches with enriched metadata, filtered by BranchQuery criteria.
9391
9492
This is the canonical query for getting branches - used by both RepoPage and IndexPage.
9593
9694
- Nothing repo: all repos (IndexPage)
9795
- Just repo: single repo (RepoPage)
9896
- Filter by branch name if provided
97+
- Filter by build status (Nothing = all, Just True = unbuilt only, Just False = built only)
9998
- Sorted by activity time (most recent first)
10099
-}
101-
getAllBranchesA :: Maybe RepoName -> Maybe Text -> Natural -> Query ViraState [BranchDetails]
102-
getAllBranchesA mRepo mFilter limit = do
100+
queryBranchDetailsA :: BranchQuery -> Natural -> Query ViraState [BranchDetails]
101+
queryBranchDetailsA query limit = do
103102
ViraState {branches, jobs} <- ask
104-
let candidateBranches = case mRepo of
105-
Nothing -> Ix.toList branches
106-
Just repo -> Ix.toList $ branches @= repo
107-
matchesFilter branch = case mFilter of
108-
Nothing -> True
109-
Just s -> T.toLower s `T.isInfixOf` T.toLower (toText branch.branchName)
110-
enriched = enrichBranchWithJobs jobs <$> filter matchesFilter candidateBranches
111-
sorted = sortWith (Down . branchActivityTime) enriched
112-
pure $ take (fromIntegral limit) sorted
103+
pure $
104+
branches
105+
& maybe Prelude.id getEQ query.repoName
106+
& Ix.toList
107+
& filter matchesBranch
108+
& fmap (enrichBranchWithJobs jobs)
109+
& filterByBuildStatus
110+
& sortWith (Down . branchActivityTime)
111+
& take (fromIntegral limit)
112+
where
113+
matchesBranch branch = case query.branchNamePattern of
114+
Nothing -> True
115+
Just q -> T.toLower q `T.isInfixOf` T.toLower (toText branch.branchName)
116+
filterByBuildStatus = case query.neverBuilt of
117+
Nothing -> Prelude.id -- all branches
118+
Just True -> filter (\d -> d.buildState == NeverBuilt)
119+
Just False -> filter (\d -> d.buildState /= NeverBuilt)
120+
121+
-- | Get single branch with enriched metadata
122+
getBranchDetailsA :: RepoName -> BranchName -> Query ViraState (Maybe BranchDetails)
123+
getBranchDetailsA repo branchName = do
124+
ViraState {branches, jobs} <- ask
125+
pure $ do
126+
branch <- Ix.getOne $ branches @= repo @= branchName
127+
pure $ enrichBranchWithJobs jobs branch
113128

114129
-- | Get all branches for a repo
115130
getRepoBranchesA :: RepoName -> Query ViraState [Branch]
@@ -123,14 +138,6 @@ getBranchByNameA repo branch = do
123138
ViraState {branches} <- ask
124139
pure $ Ix.getOne $ branches @= repo @= branch
125140

126-
-- | Get branch with enriched metadata
127-
getBranchDetailsA :: RepoName -> BranchName -> Query ViraState (Maybe BranchDetails)
128-
getBranchDetailsA repo branchName = do
129-
ViraState {branches, jobs} <- ask
130-
case Ix.getOne $ branches @= repo @= branchName of
131-
Nothing -> pure Nothing
132-
Just branch -> pure $ Just $ enrichBranchWithJobs jobs branch
133-
134141
-- | Set a repository's refresh status
135142
setRefreshStatusA :: RepoName -> Maybe RefreshResult -> Update ViraState ()
136143
setRefreshStatusA name mResult = do
@@ -314,7 +321,7 @@ $( makeAcidic
314321
[ 'setAllReposA
315322
, 'getAllReposA
316323
, 'getRepoByNameA
317-
, 'getAllBranchesA
324+
, 'queryBranchDetailsA
318325
, 'getRepoBranchesA
319326
, 'getBranchByNameA
320327
, 'getBranchDetailsA

packages/vira/src/Vira/State/Type.hs

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module Vira.State.Type where
77

88
import Data.Aeson (FromJSON, ToJSON)
99
import Data.Data (Data)
10+
import Data.Default (Default (..))
1011
import Data.IxSet.Typed
1112
import Data.SafeCopy
1213
import Data.Time (UTCTime)
@@ -61,20 +62,44 @@ instance Indexable BranchIxs Branch where
6162
(ixFun $ \Branch {repoName} -> [repoName])
6263
(ixFun $ \Branch {branchName} -> [branchName])
6364

64-
-- | Badge state for 'Branch' status
65-
data BadgeState = NeverBuilt | OutOfDate
65+
-- | Build freshness indicator for branches that have been built
66+
data BuildFreshness
67+
= -- | Latest job commit matches head commit
68+
UpToDate
69+
| -- | Latest job commit differs from head commit
70+
OutOfDate
6671
deriving stock (Generic, Show, Eq)
6772

73+
-- | Build state for a 'Branch'
74+
data BranchBuildState
75+
= -- | Branch has never been built
76+
NeverBuilt
77+
| -- | Branch has builds, with the latest job and freshness indicator
78+
Built Job BuildFreshness
79+
deriving stock (Generic, Show, Eq)
80+
81+
-- | Query parameters for filtering branches
82+
data BranchQuery = BranchQuery
83+
{ repoName :: Maybe RepoName
84+
-- ^ Filter by specific repository (Nothing = all repos)
85+
, branchNamePattern :: Maybe Text
86+
-- ^ Filter by branch name substring (Nothing = no name filter)
87+
, neverBuilt :: Maybe Bool
88+
-- ^ Nothing = all branches, Just True = unbuilt only, Just False = built only
89+
}
90+
deriving stock (Generic, Show, Eq)
91+
92+
instance Default BranchQuery where
93+
def = BranchQuery {repoName = Nothing, branchNamePattern = Nothing, neverBuilt = Nothing}
94+
6895
-- | 'Branch' with enriched metadata for display
6996
data BranchDetails = BranchDetails
7097
{ branch :: Branch
7198
-- ^ The 'Branch' information from the database
72-
, mLatestJob :: Maybe Job
73-
-- ^ The most recent CI 'Job' for this branch, if any
7499
, jobsCount :: Natural
75100
-- ^ Total number of 'Job's for this branch
76-
, badgeState :: Maybe BadgeState
77-
-- ^ 'BadgeState' computed from job/commit comparison
101+
, buildState :: BranchBuildState
102+
-- ^ Build state computed from job/commit comparison (includes latest job if built)
78103
}
79104
deriving stock (Generic, Show, Eq)
80105

@@ -84,9 +109,9 @@ Activity is defined as @max(head commit date, latest job created time)@.
84109
This ensures branches with recent commits OR recent builds appear first.
85110
-}
86111
branchActivityTime :: BranchDetails -> UTCTime
87-
branchActivityTime details = case details.mLatestJob of
88-
Nothing -> details.branch.headCommit.date
89-
Just job -> max details.branch.headCommit.date job.jobCreatedTime
112+
branchActivityTime details = case details.buildState of
113+
NeverBuilt -> details.branch.headCommit.date
114+
Built job _ -> max details.branch.headCommit.date job.jobCreatedTime
90115

91116
-- | Sorts 'BranchDetails' by most recent activity descending (most recent first).
92117
instance Ord BranchDetails where
@@ -181,7 +206,9 @@ $(deriveSafeCopy 0 'base ''JobStatus)
181206
$(deriveSafeCopy 0 'base ''JobId)
182207
$(deriveSafeCopy 0 'base ''Job)
183208
$(deriveSafeCopy 1 'base ''Branch)
184-
$(deriveSafeCopy 0 'base ''BadgeState)
209+
$(deriveSafeCopy 0 'base ''BuildFreshness)
210+
$(deriveSafeCopy 0 'base ''BranchBuildState)
211+
$(deriveSafeCopy 0 'base ''BranchQuery)
185212
$(deriveSafeCopy 0 'base ''BranchDetails)
186213
$(deriveSafeCopy 0 'base ''Repo)
187214

@@ -190,4 +217,4 @@ The version is automatically used by the @--auto-reset-state@ feature to detect
190217
When enabled, auto-reset will remove @ViraState/@ and @workspace/*/jobs@ directories on mismatch.
191218
Run @vira info@ to see the current schema version.
192219
-}
193-
$(deriveSafeCopy 6 'base ''ViraState)
220+
$(deriveSafeCopy 7 'base ''ViraState)

packages/vira/src/Vira/Web/LinkTo/Resolve.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import Vira.Web.Servant ((//), (/:))
1717
-- | Resolve a 'LinkTo' into a servant 'Link'
1818
linkTo :: LinkTo -> Link
1919
linkTo = \case
20-
Home -> fieldLink _home
20+
Home neverBuilt -> fieldLink _home neverBuilt
2121
RepoListing -> fieldLink _repos // RegistryPage._listing
2222
Repo name -> fieldLink _repos // RegistryPage._repo /: name // RepoPage._view
2323
RepoUpdate name -> fieldLink _repos // RegistryPage._repo /: name // RepoPage._update

packages/vira/src/Vira/Web/LinkTo/Type.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Avoids cyclic imports by providing a type-level hint instead of direct route ref
1212
Used with 'Vira.Web.Lucid.getLink' to generate 'Servant.Links.Link' values.
1313
-}
1414
data LinkTo
15-
= Home
15+
= Home (Maybe Bool) -- Nothing = all, Just True = unbuilt, Just False = built
1616
| RepoListing
1717
| Repo RepoName
1818
| RepoUpdate RepoName
@@ -32,7 +32,7 @@ data LinkTo
3232

3333
linkShortTitle :: LinkTo -> Text
3434
linkShortTitle = \case
35-
Home -> "Vira"
35+
Home _ -> "Vira"
3636
RepoListing -> "Repositories"
3737
Repo name -> toStringText name
3838
RepoUpdate _ -> "Update" -- unused

packages/vira/src/Vira/Web/Pages/BranchPage.hs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Vira.App qualified as App
1212
import Vira.App.CLI (WebSettings)
1313
import Vira.State.Acid qualified as St
1414
import Vira.State.Core qualified as St
15-
import Vira.State.Type (BadgeState (..), BranchDetails (..))
15+
import Vira.State.Type (BranchBuildState (..), BranchDetails (..), BuildFreshness (..))
1616
import Vira.Web.LinkTo.Type qualified as LinkTo
1717
import Vira.Web.Lucid (AppHtml, getLink, runAppHtml)
1818
import Vira.Web.Stack qualified as Web
@@ -66,12 +66,12 @@ viewBranch repo branchDetails jobs = do
6666
div_ [class_ "w-3 h-3 mr-1 flex items-center justify-center"] $ toHtmlRaw Icon.alert_triangle
6767
"Branch deleted from remote"
6868
-- Out of date badge
69-
whenJust branchDetails.badgeState $ \case
70-
OutOfDate ->
69+
case branchDetails.buildState of
70+
Built _ OutOfDate ->
7171
span_ [class_ "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300"] $ do
7272
div_ [class_ "w-3 h-3 mr-1 flex items-center justify-center"] $ toHtmlRaw Icon.clock
7373
"Out of date"
74-
NeverBuilt -> mempty -- Don't show in header
74+
_ -> mempty -- Don't show badge for NeverBuilt or UpToDate
7575
div_ [class_ "flex items-center gap-2"] $ do
7676
buildLink <- lift $ getLink $ LinkTo.Build repo.name branchDetails.branch.branchName
7777
W.viraRequestButton_

packages/vira/src/Vira/Web/Pages/IndexPage.hs

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
-- | Top-level routes and views
22
module Vira.Web.Pages.IndexPage where
33

4+
import Data.Default (def)
45
import Lucid
5-
import Servant.API (Get, NamedRoutes, (:>))
6+
import Servant.API (Get, NamedRoutes, QueryParam, (:>))
67
import Servant.API.ContentTypes.Lucid (HTML)
78
import Servant.API.Generic (GenericMode (type (:-)))
89
import Servant.Links (fieldLink, linkURI)
910
import Servant.Server.Generic (AsServer)
1011
import Vira.App qualified as App
1112
import Vira.State.Acid qualified as St
12-
import Vira.State.Type qualified as St
13-
import Vira.Web.Lucid (AppHtml, runAppHtml)
13+
import Vira.State.Type (BranchQuery (..))
14+
import Vira.Web.LinkTo.Type (LinkTo (..))
15+
import Vira.Web.Lucid (AppHtml, getLinkUrl, runAppHtml)
1416
import Vira.Web.Pages.CachePage qualified as CachePage
1517
import Vira.Web.Pages.EnvironmentPage qualified as EnvironmentPage
1618
import Vira.Web.Pages.EventsPage qualified as EventsPage
@@ -21,11 +23,12 @@ import Vira.Web.Stack qualified as Web
2123
import Vira.Web.Stream.ScopedRefresh qualified as Refresh
2224
import Vira.Web.Widgets.JobsListing qualified as W
2325
import Vira.Web.Widgets.Layout qualified as W
26+
import Vira.Web.Widgets.Tabs (TabItem (..), viraTabs_)
2427
import Web.TablerIcons.Outline qualified as Icon
2528
import Prelude hiding (Reader, ask, runReader)
2629

2730
data Routes mode = Routes
28-
{ _home :: mode :- Get '[HTML] (Html ())
31+
{ _home :: mode :- QueryParam "neverBuilt" Bool :> Get '[HTML] (Html ())
2932
, _repos :: mode :- "r" Servant.API.:> NamedRoutes RegistryPage.Routes
3033
, _jobs :: mode :- "j" Servant.API.:> NamedRoutes JobPage.Routes
3134
, _environment :: mode :- "env" Servant.API.:> NamedRoutes EnvironmentPage.Routes
@@ -40,8 +43,9 @@ handlers :: App.GlobalSettings -> App.ViraRuntimeState -> App.WebSettings -> Rou
4043
handlers globalSettings viraRuntimeState webSettings =
4144
Routes
4245
{ _home =
43-
Web.runAppInServant globalSettings viraRuntimeState webSettings $
44-
runAppHtml indexView
46+
Web.runAppInServant globalSettings viraRuntimeState webSettings
47+
. runAppHtml
48+
. indexView
4549
, _repos = RegistryPage.handlers globalSettings viraRuntimeState webSettings
4650
, _jobs = JobPage.handlers globalSettings viraRuntimeState webSettings
4751
, _environment = EnvironmentPage.handlers globalSettings viraRuntimeState webSettings
@@ -54,24 +58,43 @@ handlers globalSettings viraRuntimeState webSettings =
5458
activityLimit :: Natural
5559
activityLimit = 15
5660

57-
indexView :: AppHtml ()
58-
indexView = do
61+
indexView :: Maybe Bool -> AppHtml ()
62+
indexView mUnbuilt = do
5963
logoUrl <- W.appLogoUrl
60-
activities <- lift $ App.query (St.GetAllBranchesA Nothing Nothing activityLimit)
6164
let linkText = show . linkURI
6265
reposLink = linkText $ fieldLink _repos // RegistryPage._listing
6366
envLink = linkText $ fieldLink _environment // EnvironmentPage._view
6467
cacheLink = linkText $ fieldLink _cache // CachePage._view
6568
W.layout mempty $ do
6669
heroWelcome logoUrl reposLink envLink cacheLink
67-
unless (null activities) $
68-
viewRecentActivity activities
70+
viewRecentActivity mUnbuilt
71+
72+
viewRecentActivity :: Maybe Bool -> AppHtml ()
73+
viewRecentActivity mNeverBuilt = do
74+
-- Get filtered activities based on neverBuilt flag
75+
let query = def {neverBuilt = mNeverBuilt}
76+
activities <- lift $ App.query (St.QueryBranchDetailsA query activityLimit)
77+
-- Calculate unbuilt count for badge
78+
let unbuiltQuery = def {neverBuilt = Just True}
79+
unbuiltCount <- length <$> lift (App.query (St.QueryBranchDetailsA unbuiltQuery activityLimit))
6980

70-
viewRecentActivity :: [St.BranchDetails] -> AppHtml ()
71-
viewRecentActivity activities = do
7281
W.viraSection_ [] $ do
73-
h2_ [class_ "text-2xl font-bold text-gray-900 dark:text-gray-100 mb-6"] "Recent Activity"
74-
div_ [] $ do
82+
-- Header with title
83+
h2_ [class_ "text-2xl font-bold text-gray-900 dark:text-gray-100"] "Recent Activity"
84+
85+
-- Tab bar
86+
allUrl <- lift $ getLinkUrl (Home Nothing)
87+
buildsUrl <- lift $ getLinkUrl (Home (Just False))
88+
unbuiltUrl <- lift $ getLinkUrl (Home (Just True))
89+
viraTabs_
90+
[]
91+
[ TabItem "All" allUrl (isNothing mNeverBuilt) Nothing
92+
, TabItem "Builds" buildsUrl (mNeverBuilt == Just False) Nothing
93+
, TabItem "Unbuilt" unbuiltUrl (mNeverBuilt == Just True) (Just unbuiltCount)
94+
]
95+
96+
-- Activity list
97+
div_ $ do
7598
forM_ activities $ \details ->
7699
W.viraBranchDetailsRow_ True details
77100

packages/vira/src/Vira/Web/Pages/RepoPage.hs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module Vira.Web.Pages.RepoPage (
55
handlers,
66
) where
77

8+
import Data.Default (def)
89
import Effectful (Eff)
910
import Effectful.Colog.Simple (withLogContext)
1011
import Effectful.Error.Static (throwError)
@@ -23,7 +24,7 @@ import Vira.Refresh qualified as Refresh
2324
import Vira.Refresh.Type (RefreshPriority (Now))
2425
import Vira.State.Acid qualified as St
2526
import Vira.State.Core qualified as St
26-
import Vira.State.Type (BranchDetails (..))
27+
import Vira.State.Type (BranchDetails (..), BranchQuery (..))
2728
import Vira.Web.LinkTo.Type qualified as LinkTo
2829
import Vira.Web.Lucid (AppHtml, getLink, getLinkUrl, runAppHtml)
2930
import Vira.Web.Stack qualified as Web
@@ -62,14 +63,16 @@ handlers globalSettings viraRuntimeState webSettings name = do
6263
viewHandler :: RepoName -> AppHtml ()
6364
viewHandler name = do
6465
repo <- lift $ App.query (St.GetRepoByNameA name) >>= maybe (throwError err404) pure
65-
branchDetails <- lift $ App.query (St.GetAllBranchesA (Just name) Nothing (fromIntegral maxBranchesDisplayed + 1))
66+
let query = def {repoName = Just name}
67+
branchDetails <- lift $ App.query (St.QueryBranchDetailsA query (fromIntegral maxBranchesDisplayed + 1))
6668
let isPruned = length branchDetails > maxBranchesDisplayed
6769
displayed = take maxBranchesDisplayed branchDetails
6870
W.layout (crumbs <> [LinkTo.Repo name]) $ viewRepo repo displayed isPruned
6971

7072
filterBranchesHandler :: RepoName -> Maybe Text -> AppHtml ()
7173
filterBranchesHandler name mQuery = do
72-
branchDetails <- lift $ App.query (St.GetAllBranchesA (Just name) mQuery (fromIntegral maxBranchesDisplayed + 1))
74+
let query = def {repoName = Just name, branchNamePattern = mQuery}
75+
branchDetails <- lift $ App.query (St.QueryBranchDetailsA query (fromIntegral maxBranchesDisplayed + 1))
7376
let isPruned = length branchDetails > maxBranchesDisplayed
7477
displayed = take maxBranchesDisplayed branchDetails
7578
_ <- lift $ App.query (St.GetRepoByNameA name) >>= maybe (throwError err404) pure

0 commit comments

Comments
 (0)