Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add issues pagination #4

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jiff = { version = "0.1.14", features = ["serde"] }
# https://github.com/lambda-fairy/maud/issues/392
maud = { version = "0.26.0", git = "https://github.com/untitaker/maud", branch = "hotreload-prototype-v2" }
memory-serve = "0.6.0"
regex = "1.11.1"
reqwest = { version = "0.12.9", features = ["json"] }
schnellru = "0.2.3"
serde = { version = "1.0.214", features = ["derive"] }
Expand All @@ -32,3 +33,4 @@ tokio = { version = "1.41.0", features = ["full"] }
tower-sessions = "0.13.0"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
url = "2.5.4"
13 changes: 13 additions & 0 deletions src/api_helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use regex::Regex;
use std::sync::LazyLock;

// one day we will be be able to use typed headers for pagination:
// https://github.com/hyperium/headers/pull/113
// https://github.com/XAMPPRocky/octocrab/issues/110#issuecomment-1458449662
static LINK_REL_NEXT_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"<(.+?)>; rel="next"; results="true""#).unwrap());

pub fn get_next_link(res: &reqwest::Response) -> Option<String> {
let link_header = res.headers().get("Link")?.to_str().ok()?;
Some(LINK_REL_NEXT_RE.captures(link_header)?[1].to_owned())
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use axum::{
use memory_serve::{load_assets, MemoryServe};
use tower_sessions::{Expiry, MemoryStore, SessionManagerLayer};

mod api_helpers;
mod routes;
mod views;

Expand Down
20 changes: 20 additions & 0 deletions src/views/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,23 @@ impl IntoResponse for Html {
(headers, self.0.into_string()).into_response()
}
}

pub fn paginated_response(next_link: Option<&str>, page_markup: Markup) -> Markup {
html! {
div.page {
(page_markup)

@if let Some(link) = next_link {
hr;

@let href = format!("?{}", url::form_urlencoded::Serializer::new(String::new())
.append_pair("next_link", link)
.finish());

a href=(href) hx-get=(href) hx-trigger="revealed" hx-swap="outerHTML" hx-select=".page > *" {
"next page"
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/views/issue_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ pub struct UpdateParams {

impl UpdateParams {
/// Convert to the structure that our API expects for status updates.
fn to_api(self) -> ApiUpdate {
fn to_api(&self) -> ApiUpdate {
match self.status {
StatusParam::Unresolved => ApiUpdate {
status: "unresolved".to_string(),
Expand Down
64 changes: 39 additions & 25 deletions src/views/organization_details.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use crate::views::helpers::html;
use axum::extract::Query;
use axum::response::IntoResponse;
use serde::Deserialize;

use crate::views::helpers::{breadcrumbs, wrap_admin_template, Html, LayoutOptions};
use crate::api_helpers::get_next_link;
use crate::views::helpers::html;
use crate::views::helpers::{
breadcrumbs, paginated_response, wrap_admin_template, Html, LayoutOptions,
};
use crate::{Error, SentryToken};

#[derive(Deserialize)]
Expand All @@ -14,22 +18,30 @@ struct ApiProject {
is_bookmarked: bool,
}

#[derive(Deserialize)]
pub struct Params {
#[serde(default)]
next_link: Option<String>,
}

pub async fn organization_details(
route: crate::routes::OrganizationDetails,
token: SentryToken,
Query(params): Query<Params>,
) -> Result<impl IntoResponse, Error> {
let org = route.org;

let client = token.client()?;
let mut response: Vec<ApiProject> = client
.get(format!(
"https://sentry.io/api/0/organizations/{org}/projects/"
))
.send()
.await?
.error_for_status()?
.json()
.await?;
let http_response =
client
.get(params.next_link.unwrap_or_else(|| {
format!("https://sentry.io/api/0/organizations/{org}/projects/")
}))
.send()
.await?
.error_for_status()?;
let next_link = get_next_link(&http_response);
let mut response: Vec<ApiProject> = http_response.json().await?;

response.sort_by_key(|p| !p.is_bookmarked);

Expand All @@ -43,25 +55,27 @@ pub async fn organization_details(
(org) ": projects"
}))

ul {
@for project in response {
li {
a preload="mouseover" href=(
crate::routes::ProjectDetails { org: org.clone(), proj: project.slug.clone() }
) {
(project.name)
}
(paginated_response(next_link.as_deref(), html! {
ul {
@for project in response {
li {
a preload="mouseover" href=(
crate::routes::ProjectDetails { org: org.clone(), proj: project.slug.clone() }
) {
(project.name)
}

@if project.is_bookmarked {
span title="bookmarked" { "📌" }
}
@if project.is_bookmarked {
span title="bookmarked" { "📌" }
}

small {
" (" (org) "/" (project.slug) ")"
small {
" (" (org) "/" (project.slug) ")"
}
}
}
}
}
}))
},
);

Expand Down
57 changes: 30 additions & 27 deletions src/views/project_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ use jiff::Timestamp;
use maud::Markup;
use serde::Deserialize;

use crate::api_helpers::get_next_link;
use crate::routes::{IssueDetails, OrganizationDetails, ProjectDetails};
use crate::views::helpers::{
breadcrumbs, event_count, html, print_relative_time, wrap_admin_template, Html, LayoutOptions,
breadcrumbs, event_count, html, paginated_response, print_relative_time, wrap_admin_template,
Html, LayoutOptions,
};
use crate::{Error, SentryToken};

Expand Down Expand Up @@ -34,6 +36,8 @@ struct ApiProject {
pub struct SearchQuery {
#[serde(default)]
query: Option<String>,
#[serde(default)]
next_link: Option<String>,
}

pub async fn project_details(
Expand All @@ -49,16 +53,17 @@ pub async fn project_details(
.query
.as_deref()
.unwrap_or("is:unresolved issue.priority:[high, medium]");
let response: Vec<ApiIssue> = client
.get(format!(
"https://sentry.io/api/0/projects/{org}/{proj}/issues/"
))
.query(&[("query", query), ("limit", "25")])
.send()
.await?
.error_for_status()?
.json()
.await?;
let http_response =
client
.get(params.next_link.unwrap_or_else(|| {
format!("https://sentry.io/api/0/projects/{org}/{proj}/issues/")
}))
.query(&[("query", query), ("limit", "25")])
.send()
.await?
.error_for_status()?;
let next_link = get_next_link(&http_response);
let response: Vec<ApiIssue> = http_response.json().await?;

let project_id = response
.first()
Expand Down Expand Up @@ -102,7 +107,7 @@ pub async fn project_details(
}
}

(render_issuestream(&org, &proj, &response))
(paginated_response(next_link.as_deref(), render_issuestream(&org, &proj, &response)))
},
);

Expand All @@ -117,30 +122,28 @@ fn render_issuestream(org: &str, proj: &str, response: &[ApiIssue]) -> Markup {
span data-level=(issue.level) { (issue.level) ": "}
(issue.title)

br;
br;

small.secondary {
(event_count(&issue.count))
", last seen "
(print_relative_time(issue.last_seen))
" ago"

@if !issue.culprit.is_empty() {
", in "
code { (issue.culprit) }
} @else if let Some(ref logger) = issue.logger {
", logged via "
code { (logger) }
}
", last seen "
(print_relative_time(issue.last_seen))
" ago"

@if !issue.culprit.is_empty() {
", in "
code { (issue.culprit) }
} @else if let Some(ref logger) = issue.logger {
", logged via "
code { (logger) }
}
}
}
}
}

@if response.is_empty() {
p {
"nothing found."
}
"nothing found."
}
}
}
10 changes: 10 additions & 0 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,13 @@ small.secondary {
font-size: 0.8em;
font-style: italic;
}


[data-hx-revealed=true] {
text-decoration: none;
color: var(--pico-secondary);
}

[data-hx-revealed=true]:after {
content: " is loading"
}