Add paginated panel loading

This commit is contained in:
Georg Bauer
2026-07-31 15:02:21 +02:00
parent 228879da15
commit 33f9eb809c
11 changed files with 1104 additions and 290 deletions

View File

@@ -10,12 +10,12 @@ use crate::{
diff,
domain::{
HistoryCommit, HomeData, IssueDetails, IssueDraft, IssueEditorData, MilestoneDetails,
PullDetails, RepositoryData, Server, api_date,
PAGE_SIZE, Page, PullDetails, RepositoryData, Server, api_date,
},
presentation::compact_date,
};
pub async fn load_repositories(server: &Server) -> Result<Vec<RepositoryData>, String> {
pub async fn load_repositories(server: &Server, page: i32) -> Result<Page<RepositoryData>, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
@@ -24,36 +24,41 @@ pub async fn load_repositories(server: &Server) -> Result<Vec<RepositoryData>, S
.await
.map_err(|error| error.to_string())?;
let login = user.login.unwrap_or_default();
let repositories = apis::user_api::user_current_list_repos(&configuration, Some(1), Some(100))
.await
.map_err(|error| error.to_string())?;
Ok(repositories
.into_iter()
.filter(|repository| {
repository
.owner
.as_ref()
.and_then(|owner| owner.login.as_deref())
== Some(login.as_str())
})
.filter_map(|repository| {
Some(RepositoryData {
owner: repository.owner?.login?,
name: repository.name?,
description: repository
.description
.filter(|text| !text.is_empty())
.unwrap_or_else(|| "No description".into()),
language: repository
.language
.filter(|text| !text.is_empty())
.unwrap_or_else(|| "Unknown language".into()),
open_issues: repository.open_issues_count.unwrap_or_default(),
updated: compact_date(repository.updated_at.as_deref()),
default_branch: repository.default_branch.unwrap_or_else(|| "main".into()),
let repositories =
apis::user_api::user_current_list_repos(&configuration, Some(page), Some(PAGE_SIZE))
.await
.map_err(|error| error.to_string())?;
let has_more = repositories.len() == PAGE_SIZE as usize;
Ok(Page {
items: repositories
.into_iter()
.filter(|repository| {
repository
.owner
.as_ref()
.and_then(|owner| owner.login.as_deref())
== Some(login.as_str())
})
})
.collect())
.filter_map(|repository| {
Some(RepositoryData {
owner: repository.owner?.login?,
name: repository.name?,
description: repository
.description
.filter(|text| !text.is_empty())
.unwrap_or_else(|| "No description".into()),
language: repository
.language
.filter(|text| !text.is_empty())
.unwrap_or_else(|| "Unknown language".into()),
open_issues: repository.open_issues_count.unwrap_or_default(),
updated: compact_date(repository.updated_at.as_deref()),
default_branch: repository.default_branch.unwrap_or_else(|| "main".into()),
})
})
.collect(),
has_more,
})
}
pub async fn load_issues(
@@ -63,7 +68,8 @@ pub async fn load_issues(
status: &str,
labels: &[String],
milestone: &str,
) -> Result<Vec<models::Issue>, String> {
page: i32,
) -> Result<Page<models::Issue>, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let labels = (!labels.is_empty()).then(|| labels.join(","));
@@ -81,10 +87,33 @@ pub async fn load_issues(
None,
None,
None,
Some(1),
Some(100),
Some(page),
Some(PAGE_SIZE),
)
.await
.map(Page::from_items)
.map_err(|error| error.to_string())
}
pub async fn load_milestones_page(
server: &Server,
owner: &str,
repository: &str,
page: i32,
) -> Result<Page<models::Milestone>, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
apis::issue_api::issue_get_milestones_list(
&client.configuration(),
owner,
repository,
Some("all"),
None,
Some(page),
Some(PAGE_SIZE),
)
.await
.map(Page::from_items)
.map_err(|error| error.to_string())
}
@@ -151,6 +180,7 @@ pub async fn load_milestone(
owner: &str,
repository: &str,
id: i64,
page: i32,
) -> Result<MilestoneDetails, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
@@ -174,12 +204,14 @@ pub async fn load_milestone(
None,
None,
None,
Some(1),
Some(100),
Some(page),
Some(PAGE_SIZE),
)
};
let (issues, pulls) = tokio::join!(load("issues"), load("pulls"));
let issues = issues.map_err(|error| error.to_string())?;
let mut pulls = pulls.map_err(|error| error.to_string())?;
let has_more = issues.len() == PAGE_SIZE as usize || pulls.len() == PAGE_SIZE as usize;
for pull in &mut pulls {
pull.repository.get_or_insert_with(|| {
Box::new(models::RepositoryMeta {
@@ -191,8 +223,9 @@ pub async fn load_milestone(
}
Ok(MilestoneDetails {
milestone,
issues: issues.map_err(|error| error.to_string())?,
issues,
pulls,
has_more,
})
}
@@ -200,7 +233,8 @@ pub async fn load_pulls(
server: &Server,
status: &str,
milestone: &str,
) -> Result<Vec<models::Issue>, String> {
page: i32,
) -> Result<Page<models::Issue>, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
@@ -210,42 +244,35 @@ pub async fn load_pulls(
.map_err(|error| error.to_string())?
.login
.ok_or("The server account has no username.")?;
let mut pulls = Vec::new();
for page in 1.. {
let batch = apis::issue_api::issue_search_issues(
&configuration,
Some(status),
None,
(!milestone.is_empty()).then_some(milestone),
None,
None,
Some("pulls"),
None,
None,
None,
None,
None,
None,
None,
Some(&owner),
None,
Some(page),
Some(100),
)
.await
.map_err(|error| error.to_string())?;
if batch.is_empty() {
break;
}
pulls.extend(batch);
}
Ok(pulls)
apis::issue_api::issue_search_issues(
&configuration,
Some(status),
None,
(!milestone.is_empty()).then_some(milestone),
None,
None,
Some("pulls"),
None,
None,
None,
None,
None,
None,
None,
Some(&owner),
None,
Some(page),
Some(PAGE_SIZE),
)
.await
.map(Page::from_items)
.map_err(|error| error.to_string())
}
pub async fn load_pull_milestones(server: &Server) -> Result<Vec<String>, String> {
let (open, closed) = tokio::try_join!(
load_pulls(server, "open", ""),
load_pulls(server, "closed", "")
load_all_pulls(server, "open"),
load_all_pulls(server, "closed")
)?;
Ok(open
.into_iter()
@@ -256,6 +283,18 @@ pub async fn load_pull_milestones(server: &Server) -> Result<Vec<String>, String
.collect())
}
async fn load_all_pulls(server: &Server, status: &str) -> Result<Vec<models::Issue>, String> {
let mut pulls = Vec::new();
for page in 1.. {
let batch = load_pulls(server, status, "", page).await?;
pulls.extend(batch.items);
if !batch.has_more {
break;
}
}
Ok(pulls)
}
pub async fn load_branches(
server: &Server,
owner: &str,
@@ -336,11 +375,14 @@ pub async fn load_branch_commits(
owner: &str,
repository: &str,
branch: &str,
) -> Result<Vec<HistoryCommit>, String> {
load_commits_for_ref(server, owner, repository, Some(branch))
pages: u32,
) -> Result<Page<HistoryCommit>, String> {
load_commits_for_ref(server, owner, repository, Some(branch), pages)
.await
.map(|commits| {
commits
.map(|page| Page {
has_more: page.has_more,
items: page
.items
.into_iter()
.map(|commit| HistoryCommit {
commit,
@@ -350,7 +392,7 @@ pub async fn load_branch_commits(
connections: Vec::new(),
refs: Vec::new(),
})
.collect()
.collect(),
})
}
@@ -359,13 +401,15 @@ pub async fn load_all_commits(
owner: &str,
repository: &str,
branches: &[String],
) -> Result<Vec<HistoryCommit>, String> {
pages: u32,
) -> Result<Page<HistoryCommit>, String> {
let mut tasks = JoinSet::new();
for (lane, branch) in branches.iter().cloned().enumerate() {
let (server, owner, repository) =
(server.clone(), owner.to_string(), repository.to_string());
tasks.spawn(async move {
let commits = load_commits_for_ref(&server, &owner, &repository, Some(&branch)).await?;
let commits =
load_commits_for_ref(&server, &owner, &repository, Some(&branch), pages).await?;
Ok::<_, String>((lane, branch, commits))
});
}
@@ -375,11 +419,19 @@ pub async fn load_all_commits(
histories.push(result.map_err(|error| error.to_string())??);
}
histories.sort_by_key(|(lane, _, _)| *lane);
let has_more = histories.iter().any(|(_, _, page)| page.has_more);
let histories = histories
.into_iter()
.map(|(lane, branch, page)| (lane, branch, page.items))
.collect();
let pull_refs = load_pull_refs(server, owner, repository)
.await
.unwrap_or_default();
Ok(build_graph(histories, pull_refs))
Ok(Page {
items: build_graph(histories, pull_refs),
has_more,
})
}
pub async fn load_pull_files(
@@ -387,28 +439,21 @@ pub async fn load_pull_files(
owner: &str,
repository: &str,
number: i64,
) -> Result<Vec<models::ChangedFile>, String> {
let mut files = Vec::new();
for page in 1.. {
let batch = apis::repository_api::repo_get_pull_request_files(
configuration,
owner,
repository,
number,
None,
None,
Some(page),
Some(100),
)
.await
.map_err(|error| error.to_string())?;
let done = batch.len() < 100;
files.extend(batch);
if done {
break;
}
}
Ok(files)
page: i32,
) -> Result<Page<models::ChangedFile>, String> {
apis::repository_api::repo_get_pull_request_files(
configuration,
owner,
repository,
number,
None,
None,
Some(page),
Some(PAGE_SIZE),
)
.await
.map(Page::from_items)
.map_err(|error| error.to_string())
}
pub async fn load_issue(
@@ -416,20 +461,37 @@ pub async fn load_issue(
owner: &str,
repository: &str,
number: i64,
page: i32,
) -> Result<IssueDetails, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
let (issue, comments) = tokio::join!(
apis::issue_api::issue_get_issue(&configuration, owner, repository, number),
apis::issue_api::issue_get_comments(&configuration, owner, repository, number, None, None,),
load_issue_comments(&configuration, owner, repository, number, page),
);
Ok(IssueDetails {
issue: issue.map_err(|error| error.to_string())?,
comments: comments.map_err(|error| error.to_string())?,
has_more: false,
comments: comments?,
})
}
async fn load_issue_comments(
configuration: &apis::configuration::Configuration,
owner: &str,
repository: &str,
number: i64,
page: i32,
) -> Result<Vec<models::Comment>, String> {
if page > 1 {
return Ok(Vec::new());
}
apis::issue_api::issue_get_comments(configuration, owner, repository, number, None, None)
.await
.map_err(|error| error.to_string())
}
pub async fn load_issue_editor(
server: &Server,
owner: &str,
@@ -544,19 +606,22 @@ pub async fn load_pull(
owner: &str,
repository: &str,
number: i64,
page: i32,
) -> Result<PullDetails, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
let (pull, comments, files) = tokio::join!(
apis::repository_api::repo_get_pull_request(&configuration, owner, repository, number,),
apis::issue_api::issue_get_comments(&configuration, owner, repository, number, None, None,),
load_pull_files(&configuration, owner, repository, number),
load_issue_comments(&configuration, owner, repository, number, page),
load_pull_files(&configuration, owner, repository, number, page),
);
let files = files?;
Ok(PullDetails {
pull: pull.map_err(|error| error.to_string())?,
comments: comments.map_err(|error| error.to_string())?,
files: files.map_err(|error| error.to_string())?,
has_more: files.has_more,
comments: comments?,
files: files.items,
})
}
@@ -821,12 +886,14 @@ async fn load_commits_for_ref(
owner: &str,
repository: &str,
branch: Option<&str>,
) -> Result<Vec<models::Commit>, String> {
pages: u32,
) -> Result<Page<models::Commit>, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
let mut commits = Vec::new();
for page in 1.. {
let mut has_more = false;
for page in 1..=pages.max(1) {
let batch = apis::repository_api::repo_get_all_commits(
&configuration,
owner,
@@ -838,22 +905,25 @@ async fn load_commits_for_ref(
None,
None,
Some(false),
Some(page),
Some(100),
Some(page as i32),
Some(PAGE_SIZE),
None,
)
.await
.map_err(|error| error.to_string())?;
let done = batch.len() < 100;
has_more = batch.len() == PAGE_SIZE as usize;
commits.extend(batch);
if done {
if !has_more {
break;
}
}
Ok(commits)
Ok(Page {
items: commits,
has_more,
})
}
pub async fn load_home(server: &Server) -> Result<HomeData, String> {
pub async fn load_home(server: &Server, page: i32) -> Result<HomeData, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration();
@@ -863,34 +933,23 @@ pub async fn load_home(server: &Server) -> Result<HomeData, String> {
.map_err(|error| error.to_string())?
.login
.ok_or("The server account has no username.")?;
let activity_configuration = configuration.clone();
let activity_login = login.clone();
let activities = async move {
let mut activities = Vec::new();
for page in 1.. {
let batch = apis::user_api::user_list_activity_feeds(
&activity_configuration,
&activity_login,
Some(true),
None,
Some(page),
Some(100),
)
.await
.map_err(|error| error.to_string())?;
if batch.is_empty() {
break;
}
activities.extend(batch);
}
Ok::<_, String>(activities)
};
let activities = apis::user_api::user_list_activity_feeds(
&configuration,
&login,
Some(true),
None,
Some(page),
Some(PAGE_SIZE),
);
let (activities, heatmap) = tokio::join!(
activities,
apis::user_api::user_get_heatmap_data(&configuration, &login),
);
Ok(HomeData {
activities: activities?,
has_more: activities
.as_ref()
.is_ok_and(|items| items.len() == PAGE_SIZE as usize),
activities: activities.map_err(|error| error.to_string())?,
heatmap: heatmap.map_err(|error| error.to_string())?,
})
}