Skip to content
Merged
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
946 changes: 504 additions & 442 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions crates/frontend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ license.workspace = true
repository.workspace = true

[dependencies]
leptos = { version = "0.6", features = ["csr"] }
leptos_router = { version = "0.6", features = ["csr"] }
leptos = { version = "0.8", features = ["csr"] }
leptos_router = { version = "0.8" }
serde.workspace = true
serde_json.workspace = true
shared = { path = "../shared" }
Expand Down
20 changes: 20 additions & 0 deletions crates/frontend/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,30 @@
//! place rather than being hand-written at every call site.

use gloo_net::http::Request;
use leptos::prelude::LocalResource;
use serde::{de::DeserializeOwned, Serialize};
use std::future::Future;

pub const BASE_URL: &str = "/api/v1";

/// Reactive async resource for client-side rendering.
///
/// `LocalResource` never has to cross a thread boundary, so unlike `Resource`
/// it accepts a non-`Send` future, which is what `gloo_net` request futures
/// are. `source` is tracked reactively; when it yields a new value the
/// `fetcher` runs again.
pub fn local_resource<S, T, Fut>(
source: impl Fn() -> S + 'static,
fetcher: impl Fn(S) -> Fut + 'static,
) -> LocalResource<T>
where
S: PartialEq + 'static,
T: 'static,
Fut: Future<Output = T> + 'static,
{
LocalResource::new(move || fetcher(source()))
}

/// Build an absolute API URL from a path relative to `/api/v1`.
pub fn api_url(path: &str) -> String {
format!("{BASE_URL}{path}")
Expand Down
4 changes: 2 additions & 2 deletions crates/frontend/src/components/nav.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use leptos::*;
use leptos_router::*;
use leptos::prelude::*;
use leptos_router::hooks::*;
use wasm_bindgen::JsCast;

/// Application-wide top navigation bar.
Expand Down
19 changes: 10 additions & 9 deletions crates/frontend/src/components/repo_nav.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::api::{get_or, post, post_json_resp, WRITE_ERROR};
use leptos::*;
use leptos_router::*;
use crate::api::{get_or, local_resource, post, post_json_resp, WRITE_ERROR};
use leptos::prelude::*;
use leptos::task::spawn_local;
use leptos_router::hooks::*;
use shared::{RepoUserStatus, Repository};

/// Shared invalidation trigger for repository data.
Expand All @@ -16,7 +17,7 @@ pub struct RepoRefresh(pub RwSignal<u32>);
pub fn use_repo_refresh() -> RwSignal<u32> {
use_context::<RepoRefresh>()
.map(|r| r.0)
.unwrap_or_else(|| create_rw_signal(0))
.unwrap_or_else(|| RwSignal::new(0))
}

/// Shared repository chrome: the repository header (name + star/watch/fork
Expand All @@ -30,19 +31,19 @@ pub fn RepoNav() -> impl IntoView {
let params = use_params_map();
let navigate = use_navigate();
let location = use_location();
let owner = move || params.with(|p| p.get("owner").cloned().unwrap_or_default());
let repo = move || params.with(|p| p.get("repo").cloned().unwrap_or_default());
let owner = move || params.with(|p| p.get("owner").unwrap_or_default());
let repo = move || params.with(|p| p.get("repo").unwrap_or_default());
let base = move || format!("/repos/{}/{}", owner(), repo());
let refresh = use_repo_refresh();

let repo_data = create_resource(
let repo_data = local_resource(
move || (owner(), repo(), refresh.get()),
|(o, r, _)| async move {
get_or::<Option<Repository>>(&format!("/api/v1/repos/{}/{}", o, r), None).await
},
);

let status = create_resource(
let status = local_resource(
move || (owner(), repo(), refresh.get()),
|(o, r, _)| async move {
get_or::<RepoUserStatus>(
Expand All @@ -56,7 +57,7 @@ pub fn RepoNav() -> impl IntoView {
},
);

let (action_error, set_action_error) = create_signal(Option::<String>::None);
let (action_error, set_action_error) = signal(Option::<String>::None);
let bump = move || refresh.update(|n| *n += 1);

// Star and watch are toggles: refreshing only after a confirmed 2xx keeps the
Expand Down
117 changes: 59 additions & 58 deletions crates/frontend/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::components::*;
use crate::pages::*;
use leptos::*;
use leptos_router::*;
use leptos::prelude::*;
use leptos_router::components::*;
use leptos_router::path;

mod api;
mod components;
Expand All @@ -18,62 +19,62 @@ fn App() -> impl IntoView {
<Router>
<Nav/>
<main class="page">
<Routes>
<Route path="/" view=UserDashboard/>
<Route path="/explore" view=Explore/>
<Route path="/admin" view=AdminDashboard/>
<Route path="/admin/users" view=AdminUsers/>
<Route path="/search" view=Search/>
<Route path="/repos/:owner/:repo/search" view=RepoCodeSearch/>
<Route path="/packages/:owner" view=PackageList/>
<Route path="/packages/:owner/:type/:name/:version" view=PackageDetail/>
<Route path="/notifications" view=NotificationList/>
<Route path="/login" view=Login/>
<Route path="/register" view=Register/>
<Route path="/repo/create" view=CreateRepo/>
<Route path="/repo/migrate" view=MigrateRepo/>
<Route path="/org/create" view=CreateOrg/>
<Route path="/users/:username" view=UserProfile/>
<Route path="/users/:username/followers" view=UserFollowers/>
<Route path="/users/:username/following" view=UserFollowing/>
<Route path="/settings/profile" view=UserSettings/>
<Route path="/orgs/:org" view=OrgProfile/>
<Route path="/repos/:owner/:repo" view=RepoDetail/>
<Route path="/repos/:owner/:repo/issues" view=IssueList/>
<Route path="/repos/:owner/:repo/issues/:index" view=IssueDetail/>
<Route path="/repos/:owner/:repo/pulls" view=PullRequestList/>
<Route path="/repos/:owner/:repo/pulls/:index" view=PullRequestDetail/>
<Route path="/repos/:owner/:repo/compare" view=CompareView/>
<Route path="/repos/:owner/:repo/actions" view=ActionsList/>
<Route path="/repos/:owner/:repo/security" view=security::SecurityDashboard/>
<Route path="/repos/:owner/:repo/pulse" view=RepoPulse/>
<Route path="/repos/:owner/:repo/actions/workflows/:id" view=WorkflowRunsList/>
<Route path="/repos/:owner/:repo/branches" view=BranchList/>
<Route path="/repos/:owner/:repo/tags" view=TagList/>
<Route path="/repos/:owner/:repo/src/*path" view=RepoCode/>
<Route path="/repos/:owner/:repo/commits" view=CommitList/>
<Route path="/repos/:owner/:repo/commits/:sha" view=CommitDiff/>
<Route path="/repos/:owner/:repo/releases" view=ReleaseList/>
<Route path="/repos/:owner/:repo/releases/new" view=ReleaseCreate/>
<Route path="/repos/:owner/:repo/releases/:id" view=ReleaseDetail/>
<Route path="/repos/:owner/:repo/labels" view=LabelList/>
<Route path="/repos/:owner/:repo/milestones" view=MilestoneList/>
<Route path="/repos/:owner/:repo/milestones/:index" view=MilestoneDetail/>
<Route path="/repos/:owner/:repo/projects" view=ProjectList/>
<Route path="/repos/:owner/:repo/projects/:id" view=ProjectDetail/>
<Route path="/repos/:owner/:repo/discussions" view=DiscussionList/>
<Route path="/repos/:owner/:repo/discussions/:id" view=DiscussionDetail/>
<Route path="/repos/:owner/:repo/wiki" view=Wiki/>
<Route path="/repos/:owner/:repo/wiki/pages/:page_name" view=Wiki/>
<Route path="/repos/:owner/:repo/wiki/pages/:page_name/edit" view=WikiEdit/>
<Route path="/repos/:owner/:repo/edit/*path" view=FileEdit/>
<Route path="/repos/:owner/:repo/settings" view=RepoSettings/>
<Route path="/repos/:owner/:repo/collaborators" view=CollaboratorList/>
<Route path="/repos/:owner/:repo/settings/webhooks" view=WebhookList/>
<Route path="/repos/:owner/:repo/settings/secrets" view=SecretList/>
<Route path="/repos/:owner/:repo/settings/keys" view=DeployKeyList/>
<Route path="/repos/:owner/:repo/settings/branches" view=ProtectedBranchList/>
<Route path="/repos/:owner/:repo/settings/lfs" view=LfsLockList/>
<Routes fallback=|| view! { <div class="not-found">"404 - Page not found"</div> }>
<Route path=path!("/") view=UserDashboard/>
<Route path=path!("/explore") view=Explore/>
<Route path=path!("/admin") view=AdminDashboard/>
<Route path=path!("/admin/users") view=AdminUsers/>
<Route path=path!("/search") view=Search/>
<Route path=path!("/repos/:owner/:repo/search") view=RepoCodeSearch/>
<Route path=path!("/packages/:owner") view=PackageList/>
<Route path=path!("/packages/:owner/:type/:name/:version") view=PackageDetail/>
<Route path=path!("/notifications") view=NotificationList/>
<Route path=path!("/login") view=Login/>
<Route path=path!("/register") view=Register/>
<Route path=path!("/repo/create") view=CreateRepo/>
<Route path=path!("/repo/migrate") view=MigrateRepo/>
<Route path=path!("/org/create") view=CreateOrg/>
<Route path=path!("/users/:username") view=UserProfile/>
<Route path=path!("/users/:username/followers") view=UserFollowers/>
<Route path=path!("/users/:username/following") view=UserFollowing/>
<Route path=path!("/settings/profile") view=UserSettings/>
<Route path=path!("/orgs/:org") view=OrgProfile/>
<Route path=path!("/repos/:owner/:repo") view=RepoDetail/>
<Route path=path!("/repos/:owner/:repo/issues") view=IssueList/>
<Route path=path!("/repos/:owner/:repo/issues/:index") view=IssueDetail/>
<Route path=path!("/repos/:owner/:repo/pulls") view=PullRequestList/>
<Route path=path!("/repos/:owner/:repo/pulls/:index") view=PullRequestDetail/>
<Route path=path!("/repos/:owner/:repo/compare") view=CompareView/>
<Route path=path!("/repos/:owner/:repo/actions") view=ActionsList/>
<Route path=path!("/repos/:owner/:repo/security") view=security::SecurityDashboard/>
<Route path=path!("/repos/:owner/:repo/pulse") view=RepoPulse/>
<Route path=path!("/repos/:owner/:repo/actions/workflows/:id") view=WorkflowRunsList/>
<Route path=path!("/repos/:owner/:repo/branches") view=BranchList/>
<Route path=path!("/repos/:owner/:repo/tags") view=TagList/>
<Route path=path!("/repos/:owner/:repo/src/*path") view=RepoCode/>
<Route path=path!("/repos/:owner/:repo/commits") view=CommitList/>
<Route path=path!("/repos/:owner/:repo/commits/:sha") view=CommitDiff/>
<Route path=path!("/repos/:owner/:repo/releases") view=ReleaseList/>
<Route path=path!("/repos/:owner/:repo/releases/new") view=ReleaseCreate/>
<Route path=path!("/repos/:owner/:repo/releases/:id") view=ReleaseDetail/>
<Route path=path!("/repos/:owner/:repo/labels") view=LabelList/>
<Route path=path!("/repos/:owner/:repo/milestones") view=MilestoneList/>
<Route path=path!("/repos/:owner/:repo/milestones/:index") view=MilestoneDetail/>
<Route path=path!("/repos/:owner/:repo/projects") view=ProjectList/>
<Route path=path!("/repos/:owner/:repo/projects/:id") view=ProjectDetail/>
<Route path=path!("/repos/:owner/:repo/discussions") view=DiscussionList/>
<Route path=path!("/repos/:owner/:repo/discussions/:id") view=DiscussionDetail/>
<Route path=path!("/repos/:owner/:repo/wiki") view=Wiki/>
<Route path=path!("/repos/:owner/:repo/wiki/pages/:page_name") view=Wiki/>
<Route path=path!("/repos/:owner/:repo/wiki/pages/:page_name/edit") view=WikiEdit/>
<Route path=path!("/repos/:owner/:repo/edit/*path") view=FileEdit/>
<Route path=path!("/repos/:owner/:repo/settings") view=RepoSettings/>
<Route path=path!("/repos/:owner/:repo/collaborators") view=CollaboratorList/>
<Route path=path!("/repos/:owner/:repo/settings/webhooks") view=WebhookList/>
<Route path=path!("/repos/:owner/:repo/settings/secrets") view=SecretList/>
<Route path=path!("/repos/:owner/:repo/settings/keys") view=DeployKeyList/>
<Route path=path!("/repos/:owner/:repo/settings/branches") view=ProtectedBranchList/>
<Route path=path!("/repos/:owner/:repo/settings/lfs") view=LfsLockList/>
</Routes>
</main>
<footer class="site-footer">
Expand Down
32 changes: 16 additions & 16 deletions crates/frontend/src/pages/actions.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
use crate::api::{delete, get, patch_json, post, post_json, WRITE_ERROR};
use crate::api::{delete, get, local_resource, patch_json, post, post_json, WRITE_ERROR};
use crate::components::RepoNav;
use leptos::*;
use leptos_router::*;
use leptos::prelude::*;
use leptos::task::spawn_local;
use leptos_router::hooks::*;
use shared::{ActionWorkflow, CreateWorkflowRunOption, UpdateWorkflowRunOption, WorkflowRun};

#[component]
pub fn ActionsList() -> impl IntoView {
let params = use_params_map();
let owner = move || params.with(|params| params.get("owner").cloned().unwrap_or_default());
let repo_name = move || params.with(|params| params.get("repo").cloned().unwrap_or_default());
let owner = move || params.with(|params| params.get("owner").unwrap_or_default());
let repo_name = move || params.with(|params| params.get("repo").unwrap_or_default());

let workflows = create_resource(
let workflows = local_resource(
move || (owner(), repo_name()),
|(o, r)| async move {
get::<Vec<ActionWorkflow>>(&format!("/api/v1/repos/{}/{}/actions/workflows", o, r))
Expand Down Expand Up @@ -44,23 +45,22 @@ pub fn ActionsList() -> impl IntoView {
#[component]
pub fn WorkflowRunsList() -> impl IntoView {
let params = use_params_map();
let owner = move || params.with(|params| params.get("owner").cloned().unwrap_or_default());
let repo_name = move || params.with(|params| params.get("repo").cloned().unwrap_or_default());
let owner = move || params.with(|params| params.get("owner").unwrap_or_default());
let repo_name = move || params.with(|params| params.get("repo").unwrap_or_default());
let workflow_id = move || {
params.with(|params| {
params
.get("id")
.cloned()
.unwrap_or_default()
.parse::<u64>()
.unwrap_or_default()
})
};

let (refresh, set_refresh) = create_signal(0);
let (action_error, set_action_error) = create_signal(Option::<String>::None);
let (refresh, set_refresh) = signal(0);
let (action_error, set_action_error) = signal(Option::<String>::None);

let runs = create_resource(
let runs = local_resource(
move || (owner(), repo_name(), workflow_id(), refresh.get()),
|(o, r, id, _)| async move {
get::<Vec<WorkflowRun>>(&format!(
Expand Down Expand Up @@ -179,9 +179,9 @@ pub fn WorkflowRunsList() -> impl IntoView {
<div class="flex gap-sm">
<button class="rerun-btn" on:click=on_rerun>"Re-run"</button>
{if is_active {
view! { <button class="cancel-run-btn" on:click=on_cancel>"Cancel"</button> }.into_view()
view! { <button class="cancel-run-btn" on:click=on_cancel>"Cancel"</button> }.into_any()
} else {
view! { <button class="delete-run-btn" on:click=on_delete>"Delete"</button> }.into_view()
view! { <button class="delete-run-btn" on:click=on_delete>"Delete"</button> }.into_any()
}}
</div>
</div>
Expand All @@ -200,9 +200,9 @@ pub fn WorkflowRunsList() -> impl IntoView {
}
}/>
</div>
}.into_view()
}.into_any()
} else {
view! {}.into_view()
view! { <span></span> }.into_any()
}}
</li>
}
Expand Down
19 changes: 10 additions & 9 deletions crates/frontend/src/pages/admin.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use crate::api::{delete, get, get_opt, WRITE_ERROR};
use leptos::*;
use crate::api::{delete, get, get_opt, local_resource, WRITE_ERROR};
use leptos::prelude::*;
use leptos::task::spawn_local;
use shared::{AdminStats, SystemNotice, User};

#[component]
pub fn AdminDashboard() -> impl IntoView {
let stats = create_resource(
let stats = local_resource(
|| (),
|_| async move { get_opt::<AdminStats>("/api/v1/admin/stats").await },
);
Expand All @@ -22,8 +23,8 @@ pub fn AdminDashboard() -> impl IntoView {
<p>"Orgs: " {s.orgs}</p>
<p>"Issues: " {s.issues}</p>
</div>
}.into_view(),
_ => view! { <p>"No stats"</p> }.into_view()
}.into_any(),
_ => view! { <p>"No stats"</p> }.into_any()
}}
</Suspense>
<AdminNotices/>
Expand All @@ -33,10 +34,10 @@ pub fn AdminDashboard() -> impl IntoView {

#[component]
pub fn AdminUsers() -> impl IntoView {
let (refresh, set_refresh) = create_signal(0);
let (action_error, set_action_error) = create_signal(Option::<String>::None);
let (refresh, set_refresh) = signal(0);
let (action_error, set_action_error) = signal(Option::<String>::None);

let users = create_resource(
let users = local_resource(
move || refresh.get(),
|_| async move { get::<Vec<User>>("/api/v1/admin/users").await },
);
Expand Down Expand Up @@ -89,7 +90,7 @@ pub fn AdminUsers() -> impl IntoView {

#[component]
pub fn AdminNotices() -> impl IntoView {
let notices = create_resource(
let notices = local_resource(
|| (),
|_| async move { get::<Vec<SystemNotice>>("/api/v1/admin/notices").await },
);
Expand Down
Loading
Loading