Jump to…
snowfeat: given a new redesign and emulated terminal homepagerxkspqmsoknz1mo
Matt W1//! Landing page and dashboard.
Matt W2
Matt W3use axum::extract::State;
Matt W4use axum::response::{IntoResponse, Response};
Matt W5use chrono::{DateTime, Utc};
Matt W6
Matt W7use crate::error::AppResult;
Matt W8use crate::state::{AppState, CsrfToken, CurrentUser, Nonce};
Matt W9use crate::views::pages::{
Matt W10 ActivityItem, BookmarkItem, Dashboard, DashChange, FeedItem, RepoSummary, StackItem,
Matt W11};
Matt W12use crate::views::{self, Chrome};
Matt W13
Matt W14/// Repos the viewer can reach: their own, plus any they collaborate on, plus
Matt W15/// any belonging to an org they are a member of, plus public repos.
Matt W16///
Matt W17/// Visibility is enforced *in* the query rather than by filtering afterwards —
Matt W18/// a post-filter is how private repos leak into listings. `$1` is the viewer's
Matt W19/// id and `$2` is whether they are an admin.
Matt W20const VISIBLE_TO_VIEWER: &str = r#"
Matt W21 r.archived = false
Matt W22 AND (
Matt W23 r.visibility = 'public'
Matt W24 OR r.owner_user_id = $1
Matt W25 OR EXISTS (SELECT 1 FROM repo_collaborators c
Matt W26 WHERE c.repo_id = r.id AND c.user_id = $1)
Matt W27 OR EXISTS (SELECT 1 FROM org_members m
Matt W28 WHERE m.org_id = r.owner_org_id AND m.user_id = $1)
Matt W29 OR $2
Matt W30 )
Matt W31"#;
Matt W32
Matt W33/// `/` — landing page when signed out, dashboard when signed in.
Matt W34///
Matt W35/// Kept dual-mode rather than redirecting: every existing link and bookmark to
Matt W36/// `/` keeps working, and `/dashboard` below serves the same body for the
Matt W37/// design's explicit dashboard links.
Matt W38pub async fn index(
Matt W39 State(state): State<AppState>,
Matt W40 CurrentUser(user): CurrentUser,
Matt W41 CsrfToken(csrf): CsrfToken,
Matt W42 Nonce(nonce): Nonce,
Matt W43) -> AppResult<Response> {
Matt W44 let Some(user) = user else {
Matt W45 let feed = public_feed(&state).await?;
Matt W46 let repos = public_repos(&state).await?;
Matt W47 let in_flight = public_in_flight(&state).await?;
Matt W48 let bookmarks = public_bookmarks(&state).await?;
Matt W49
Matt W50 // The clone line names a repository the visitor can actually clone
Matt W51 // when there is one, and only falls back to a placeholder on an
Matt W52 // instance with nothing public in it.
Matt W53 let sample = repos.first().map(|r| format!("{}/{}", r.owner, r.name));
Matt W54 let hint = match repos.first() {
Matt W55 Some(r) => state.config.https_clone_url(&r.owner, &r.name),
Matt W56 None => state.config.https_clone_url("your-org", "your-repo"),
Matt W57 };
Matt W58
Matt W59 return Ok(views::page_full(
Matt W60 Chrome { title: "Dogfood", user: None, csrf: &csrf, nonce: &nonce },
Matt W61 views::pages::landing(views::pages::Landing {
Matt W62 clone_hint: &format!("jj git clone {hint}"),
Matt W63 sample_repo: sample.as_deref(),
Matt W64 feed: &feed,
Matt W65 repos: &repos,
Matt W66 in_flight: &in_flight,
Matt W67 bookmarks: &bookmarks,
Matt W68 }),
Matt W69 )
Matt W70 .into_response());
Matt W71 };
Matt W72
Matt W73 render_dashboard(&state, &user, &csrf, &nonce).await
Matt W74}
Matt W75
Matt W76/// `/dashboard` — the signed-in dashboard, addressable on its own.
Matt W77///
Matt W78/// A signed-out visitor gets the landing page rather than a login redirect: the
Matt W79/// dashboard is not a secret, it is just empty without an account, and bouncing
Matt W80/// somebody to an SSO round trip to learn that is worse.
Matt W81pub async fn dashboard(
Matt W82 State(state): State<AppState>,
Matt W83 CurrentUser(user): CurrentUser,
Matt W84 CsrfToken(csrf): CsrfToken,
Matt W85 Nonce(nonce): Nonce,
Matt W86) -> AppResult<Response> {
Matt W87 match user {
Matt W88 Some(user) => render_dashboard(&state, &user, &csrf, &nonce).await,
Matt W89 None => Ok(axum::response::Redirect::to("/").into_response()),
Matt W90 }
Matt W91}
Matt W92
Matt W93async fn render_dashboard(
Matt W94 state: &AppState,
Matt W95 user: &df_db::models::User,
Matt W96 csrf: &str,
Matt W97 nonce: &str,
Matt W98) -> AppResult<Response> {
Matt W99 let repos = visible_repos(state, user).await?;
Matt W100 let awaiting = awaiting_review(state, user).await?;
Matt W101 let mine = my_open_changes(state, user).await?;
Matt W102 let activity = watched_activity(state, user).await?;
Matt W103
Matt W104 Ok(views::page(
Matt W105 Chrome { title: "Dashboard", user: Some(user), csrf, nonce },
Matt W106 views::pages::dashboard(Dashboard {
Matt W107 user,
Matt W108 awaiting: &awaiting,
Matt W109 mine: &mine,
Matt W110 activity: &activity,
Matt W111 repos: &repos,
Matt W112 }),
Matt W113 )
Matt W114 .into_response())
Matt W115}
Matt W116
Matt W117// ─── queries ─────────────────────────────────────────────────────────────────
Matt W118
Matt W119type RepoRow = (
Matt W120 String,
Matt W121 String,
Matt W122 Option<String>,
Matt W123 bool,
Matt W124 i64,
Matt W125 i64,
Matt W126 Option<DateTime<Utc>>,
Matt W127);
Matt W128
Matt W129fn to_summaries(rows: Vec<RepoRow>) -> Vec<RepoSummary> {
Matt W130 rows.into_iter()
Matt W131 .map(
Matt W132 |(owner, name, description, private, open_changes, conflicted, pushed_at)| RepoSummary {
Matt W133 owner,
Matt W134 name,
Matt W135 description,
Matt W136 private,
Matt W137 open_changes,
Matt W138 conflicted,
Matt W139 pushed_at,
Matt W140 },
Matt W141 )
Matt W142 .collect()
Matt W143}
Matt W144
Matt W145/// The card's counts, as correlated subqueries.
Matt W146///
Matt W147/// A `LEFT JOIN … GROUP BY` would need two conditional aggregates over the same
Matt W148/// join and would still have to handle the no-changes case; two scalar
Matt W149/// subqueries against `(repo_id, state)` are both cheaper and easier to read.
Matt W150const REPO_CARD_COUNTS: &str = r#"
Matt W151 (SELECT count(*) FROM changes c
Matt W152 WHERE c.repo_id = r.id AND c.state = 'open') AS open_changes,
Matt W153 (SELECT count(*) FROM changes c
Matt W154 WHERE c.repo_id = r.id AND c.state = 'open' AND c.conflicted) AS conflicted,
Matt W155 r.pushed_at
Matt W156"#;
Matt W157
Matt W158async fn visible_repos(state: &AppState, user: &df_db::models::User) -> AppResult<Vec<RepoSummary>> {
Matt W159 let sql = format!(
Matt W160 r#"
Matt W161 SELECT COALESCE(ou.handle, og.handle) AS owner,
Matt W162 r.name::text,
Matt W163 r.description,
Matt W164 (r.visibility = 'private') AS private,
Matt W165 {REPO_CARD_COUNTS}
Matt W166 FROM repos r
Matt W167 LEFT JOIN users ou ON ou.id = r.owner_user_id
Matt W168 LEFT JOIN orgs og ON og.id = r.owner_org_id
Matt W169 WHERE {VISIBLE_TO_VIEWER}
Matt W170 ORDER BY r.pushed_at DESC NULLS LAST, r.created_at DESC
Matt W171 LIMIT 50
Matt W172 "#
Matt W173 );
Matt W174
Matt W175 let rows: Vec<RepoRow> = sqlx::query_as(&sql)
Matt W176 .bind(user.id)
Matt W177 .bind(user.is_admin)
Matt W178 .fetch_all(&state.db)
Matt W179 .await?;
Matt W180
Matt W181 Ok(to_summaries(rows))
Matt W182}
Matt W183
Matt W184/// Public repositories, for the signed-out landing page.
Matt W185async fn public_repos(state: &AppState) -> AppResult<Vec<RepoSummary>> {
Matt W186 let rows: Vec<RepoRow> = sqlx::query_as(&format!(
Matt W187 r#"
Matt W188 SELECT COALESCE(ou.handle, og.handle) AS owner,
Matt W189 r.name::text,
Matt W190 r.description,
Matt W191 false AS private,
Matt W192 {REPO_CARD_COUNTS}
Matt W193 FROM repos r
Matt W194 LEFT JOIN users ou ON ou.id = r.owner_user_id
Matt W195 LEFT JOIN orgs og ON og.id = r.owner_org_id
Matt W196 WHERE r.archived = false AND r.visibility = 'public'
Matt W197 ORDER BY r.pushed_at DESC NULLS LAST, r.created_at DESC
Matt W198 LIMIT 6
Matt W199 "#
Matt W200 ))
Matt W201 .fetch_all(&state.db)
Matt W202 .await?;
Matt W203
Matt W204 Ok(to_summaries(rows))
Matt W205}
Matt W206
Matt W207/// Open public changes that are part of a stack — the landing page's
Matt W208/// "in flight now".
Matt W209///
Matt W210/// "Part of a stack" is exactly "has an edge in `change_edges`": a change with
Matt W211/// a parent or a child is one somebody is building on or building from. A lone
Matt W212/// open change is work, but it is not a stack, and the panel is about the thing
Matt W213/// branches cannot represent.
Matt W214async fn public_in_flight(state: &AppState) -> AppResult<Vec<StackItem>> {
Matt W215 /// `(owner, repo, number, change_id, synthetic, title, conflicted, updated_at)`
Matt W216 type Row = (String, String, i64, String, bool, String, bool, DateTime<Utc>);
Matt W217
Matt W218 let rows: Vec<Row> = sqlx::query_as(
Matt W219 r#"
Matt W220 SELECT COALESCE(ou.handle, og.handle) AS owner,
Matt W221 r.name::text,
Matt W222 c.number,
Matt W223 c.change_id,
Matt W224 c.synthetic,
Matt W225 c.title,
Matt W226 c.conflicted,
Matt W227 c.updated_at
Matt W228 FROM changes c
Matt W229 JOIN repos r ON r.id = c.repo_id
Matt W230 LEFT JOIN users ou ON ou.id = r.owner_user_id
Matt W231 LEFT JOIN orgs og ON og.id = r.owner_org_id
Matt W232 WHERE r.archived = false
Matt W233 AND r.visibility = 'public'
Matt W234 AND c.state = 'open'
Matt W235 AND EXISTS (
Matt W236 SELECT 1 FROM change_edges e
Matt W237 WHERE e.child_change = c.id OR e.parent_change = c.id
Matt W238 )
Matt W239 ORDER BY c.updated_at DESC
Matt W240 LIMIT 4
Matt W241 "#,
Matt W242 )
Matt W243 .fetch_all(&state.db)
Matt W244 .await?;
Matt W245
Matt W246 Ok(rows
Matt W247 .into_iter()
Matt W248 .map(
Matt W249 |(owner, repo, number, change_id, synthetic, title, conflicted, when)| StackItem {
Matt W250 owner,
Matt W251 repo,
Matt W252 number,
Matt W253 change_id,
Matt W254 synthetic,
Matt W255 title,
Matt W256 conflicted,
Matt W257 when,
Matt W258 },
Matt W259 )
Matt W260 .collect())
Matt W261}
Matt W262
Matt W263/// Recently moved bookmarks across public repositories.
Matt W264async fn public_bookmarks(state: &AppState) -> AppResult<Vec<BookmarkItem>> {
Matt W265 let rows: Vec<(String, String, String, bool, DateTime<Utc>)> = sqlx::query_as(
Matt W266 r#"
Matt W267 SELECT COALESCE(ou.handle, og.handle) AS owner,
Matt W268 r.name::text,
Matt W269 b.name AS bookmark,
Matt W270 b.protected,
Matt W271 b.updated_at
Matt W272 FROM bookmarks b
Matt W273 JOIN repos r ON r.id = b.repo_id
Matt W274 LEFT JOIN users ou ON ou.id = r.owner_user_id
Matt W275 LEFT JOIN orgs og ON og.id = r.owner_org_id
Matt W276 WHERE r.archived = false AND r.visibility = 'public'
Matt W277 ORDER BY b.updated_at DESC
Matt W278 LIMIT 5
Matt W279 "#,
Matt W280 )
Matt W281 .fetch_all(&state.db)
Matt W282 .await?;
Matt W283
Matt W284 Ok(rows
Matt W285 .into_iter()
Matt W286 .map(|(owner, repo, name, protected, updated_at)| BookmarkItem {
Matt W287 owner,
Matt W288 repo,
Matt W289 name,
Matt W290 protected,
Matt W291 updated_at,
Matt W292 })
Matt W293 .collect())
Matt W294}
Matt W295
Matt W296/// The "shipping right now" feed.
Matt W297///
Matt W298/// Public repositories only, and no drafts: this renders for anonymous
Matt W299/// visitors, so anything it can reach is world-readable by definition.
Matt W300///
Matt W301/// Driven by the event log rather than by `changes.updated_at`, so each row can
Matt W302/// state what happened. The join to `changes` is an inner join on
Matt W303/// `subject_type = 'change'`, which also drops repository- and bookmark-scoped
Matt W304/// events — the feed is about work, and "somebody renamed a bookmark" is not
Matt W305/// what a visitor came to see.
Matt W306async fn public_feed(state: &AppState) -> AppResult<Vec<FeedItem>> {
Matt W307 /// `(owner, repo, number, change_id, synthetic, title, actor, author_name,
Matt W308 /// kind, created_at)`
Matt W309 type Row = (
Matt W310 String,
Matt W311 String,
Matt W312 i64,
Matt W313 String,
Matt W314 bool,
Matt W315 String,
Matt W316 Option<String>,
Matt W317 Option<String>,
Matt W318 String,
Matt W319 DateTime<Utc>,
Matt W320 );
Matt W321
Matt W322 let rows: Vec<Row> = sqlx::query_as(
Matt W323 // `hr.author_name` is the fallback when no account matched the commit's
Matt W324 // email — the person is still known, just not linkable.
Matt W325 r#"
Matt W326 SELECT COALESCE(ou.handle, og.handle) AS owner,
Matt W327 r.name::text,
Matt W328 c.number,
Matt W329 c.change_id,
Matt W330 c.synthetic,
Matt W331 c.title,
Matt W332 ac.handle AS actor,
Matt W333 hr.author_name,
Matt W334 e.kind,
Matt W335 e.created_at
Matt W336 FROM events e
Matt W337 JOIN changes c ON c.id = e.subject_id AND e.subject_type = 'change'
Matt W338 JOIN repos r ON r.id = e.repo_id
Matt W339 LEFT JOIN users ou ON ou.id = r.owner_user_id
Matt W340 LEFT JOIN orgs og ON og.id = r.owner_org_id
Matt W341 LEFT JOIN users ac ON ac.id = e.actor_id
Matt W342 LEFT JOIN revisions hr ON hr.id = c.head_revision_id
Matt W343 WHERE r.archived = false
Matt W344 AND r.visibility = 'public'
Matt W345 AND c.state <> 'draft'
Matt W346 ORDER BY e.created_at DESC
Matt W347 LIMIT 12
Matt W348 "#,
Matt W349 )
Matt W350 .fetch_all(&state.db)
Matt W351 .await?;
Matt W352
Matt W353 Ok(rows
Matt W354 .into_iter()
Matt W355 .map(
Matt W356 |(owner, repo, number, change_id, synthetic, title, actor, actor_name, kind, when)| {
Matt W357 FeedItem {
Matt W358 owner,
Matt W359 repo,
Matt W360 number,
Matt W361 change_id,
Matt W362 synthetic,
Matt W363 title,
Matt W364 actor,
Matt W365 actor_name,
Matt W366 kind,
Matt W367 when,
Matt W368 }
Matt W369 },
Matt W370 )
Matt W371 .collect())
Matt W372}
Matt W373
Matt W374type ChangeRow = (
Matt W375 String,
Matt W376 String,
Matt W377 i64,
Matt W378 String,
Matt W379 bool,
Matt W380 String,
Matt W381 String,
Matt W382 bool,
Matt W383 Option<String>,
Matt W384 Option<String>,
Matt W385 DateTime<Utc>,
Matt W386);
Matt W387
Matt W388fn to_dash_changes(rows: Vec<ChangeRow>) -> Vec<DashChange> {
Matt W389 rows.into_iter()
Matt W390 .map(
Matt W391 |(
Matt W392 owner,
Matt W393 repo,
Matt W394 number,
Matt W395 change_id,
Matt W396 synthetic,
Matt W397 title,
Matt W398 state,
Matt W399 conflicted,
Matt W400 author,
Matt W401 author_name,
Matt W402 updated_at,
Matt W403 )| {
Matt W404 DashChange {
Matt W405 owner,
Matt W406 repo,
Matt W407 number,
Matt W408 change_id,
Matt W409 synthetic,
Matt W410 title,
Matt W411 state,
Matt W412 conflicted,
Matt W413 author,
Matt W414 author_name,
Matt W415 updated_at,
Matt W416 }
Matt W417 },
Matt W418 )
Matt W419 .collect()
Matt W420}
Matt W421
Matt W422const CHANGE_COLUMNS: &str = r#"
Matt W423 SELECT COALESCE(ou.handle, og.handle) AS owner,
Matt W424 r.name::text,
Matt W425 c.number,
Matt W426 c.change_id,
Matt W427 c.synthetic,
Matt W428 c.title,
Matt W429 c.state::text,
Matt W430 c.conflicted,
Matt W431 au.handle AS author,
Matt W432 hr.author_name,
Matt W433 c.updated_at
Matt W434 FROM changes c
Matt W435 JOIN repos r ON r.id = c.repo_id
Matt W436 LEFT JOIN users ou ON ou.id = r.owner_user_id
Matt W437 LEFT JOIN orgs og ON og.id = r.owner_org_id
Matt W438 LEFT JOIN users au ON au.id = c.author_user_id
Matt W439 LEFT JOIN revisions hr ON hr.id = c.head_revision_id
Matt W440"#;
Matt W441
Matt W442/// Open changes waiting on this viewer.
Matt W443///
Matt W444/// There is no "requested reviewers" table — `reviews` records verdicts that
Matt W445/// were *given*, not ones that were asked for. So "awaiting your review" is
Matt W446/// derived: an open change in a repo you can reach, that you did not write, and
Matt W447/// that you have not reviewed at its current head revision. Re-reviewing is
Matt W448/// therefore prompted whenever the author pushes again, which is the behaviour
Matt W449/// a reviewer wants.
Matt W450async fn awaiting_review(
Matt W451 state: &AppState,
Matt W452 user: &df_db::models::User,
Matt W453) -> AppResult<Vec<DashChange>> {
Matt W454 let sql = format!(
Matt W455 r#"
Matt W456 {CHANGE_COLUMNS}
Matt W457 WHERE {VISIBLE_TO_VIEWER}
Matt W458 AND c.state = 'open'
Matt W459 AND c.author_user_id IS DISTINCT FROM $1
Matt W460 AND NOT EXISTS (
Matt W461 SELECT 1 FROM reviews rv
Matt W462 WHERE rv.change_id_fk = c.id
Matt W463 AND rv.reviewer_id = $1
Matt W464 AND rv.revision_id = c.head_revision_id
Matt W465 )
Matt W466 ORDER BY c.updated_at DESC
Matt W467 LIMIT 10
Matt W468 "#
Matt W469 );
Matt W470
Matt W471 let rows: Vec<ChangeRow> = sqlx::query_as(&sql)
Matt W472 .bind(user.id)
Matt W473 .bind(user.is_admin)
Matt W474 .fetch_all(&state.db)
Matt W475 .await?;
Matt W476
Matt W477 Ok(to_dash_changes(rows))
Matt W478}
Matt W479
Matt W480async fn my_open_changes(
Matt W481 state: &AppState,
Matt W482 user: &df_db::models::User,
Matt W483) -> AppResult<Vec<DashChange>> {
Matt W484 let sql = format!(
Matt W485 r#"
Matt W486 {CHANGE_COLUMNS}
Matt W487 WHERE {VISIBLE_TO_VIEWER}
Matt W488 AND c.author_user_id = $1
Matt W489 AND c.state IN ('open', 'draft')
Matt W490 ORDER BY c.updated_at DESC
Matt W491 LIMIT 10
Matt W492 "#
Matt W493 );
Matt W494
Matt W495 let rows: Vec<ChangeRow> = sqlx::query_as(&sql)
Matt W496 .bind(user.id)
Matt W497 .bind(user.is_admin)
Matt W498 .fetch_all(&state.db)
Matt W499 .await?;
Matt W500
Matt W501 Ok(to_dash_changes(rows))
Matt W502}
Matt W503
Matt W504/// Recent events across every repository the viewer can reach.
Matt W505async fn watched_activity(
Matt W506 state: &AppState,
Matt W507 user: &df_db::models::User,
Matt W508) -> AppResult<Vec<ActivityItem>> {
Matt W509 let sql = format!(
Matt W510 r#"
Matt W511 SELECT COALESCE(ou.handle, og.handle) AS owner,
Matt W512 r.name::text,
Matt W513 ac.handle AS actor,
Matt W514 e.kind,
Matt W515 ch.number,
Matt W516 ch.title,
Matt W517 e.created_at
Matt W518 FROM events e
Matt W519 JOIN repos r ON r.id = e.repo_id
Matt W520 LEFT JOIN users ou ON ou.id = r.owner_user_id
Matt W521 LEFT JOIN orgs og ON og.id = r.owner_org_id
Matt W522 LEFT JOIN users ac ON ac.id = e.actor_id
Matt W523 LEFT JOIN changes ch
Matt W524 ON ch.id = e.subject_id AND e.subject_type = 'change'
Matt W525 WHERE {VISIBLE_TO_VIEWER}
Matt W526 ORDER BY e.created_at DESC
Matt W527 LIMIT 15
Matt W528 "#
Matt W529 );
Matt W530
Matt W531 let rows: Vec<(
Matt W532 String,
Matt W533 String,
Matt W534 Option<String>,
Matt W535 String,
Matt W536 Option<i64>,
Matt W537 Option<String>,
Matt W538 DateTime<Utc>,
Matt W539 )> = sqlx::query_as(&sql)
Matt W540 .bind(user.id)
Matt W541 .bind(user.is_admin)
Matt W542 .fetch_all(&state.db)
Matt W543 .await?;
Matt W544
Matt W545 Ok(rows
Matt W546 .into_iter()
Matt W547 .map(
Matt W548 |(owner, repo, actor, kind, change_number, change_title, when)| ActivityItem {
Matt W549 owner,
Matt W550 repo,
Matt W551 actor,
Matt W552 kind,
Matt W553 change_number,
Matt W554 change_title,
Matt W555 when,
Matt W556 },
Matt W557 )
Matt W558 .collect())
Matt W559}

559 lines · Rust