feat: Add native Actions views #27
@@ -1,69 +1,72 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::{provide_meta_context, Title};
|
||||
use leptos_router::components::{Route, Router, Routes};
|
||||
use leptos_router::{ParamSegment, StaticSegment};
|
||||
|
||||
use crate::pages::account::Account;
|
||||
use crate::pages::account_emails::AccountEmails;
|
||||
use crate::pages::account_security::AccountSecurity;
|
||||
use crate::pages::home::Home;
|
||||
use crate::pages::login::Login;
|
||||
use crate::pages::org_agents::OrgAgents;
|
||||
use crate::pages::org_teams::OrgTeams;
|
||||
use crate::pages::profile::Profile;
|
||||
use crate::pages::repository::{NewRepository, RepositoryPage};
|
||||
use crate::pages::repository_code::{
|
||||
RepositoryBlame, RepositoryBlob, RepositoryCode, RepositoryCodeDefault,
|
||||
RepositoryCommitsDefault,
|
||||
};
|
||||
use crate::pages::repository_commits::{RepositoryCommit, RepositoryCommits};
|
||||
use crate::pages::repository_compare::RepositoryCompare;
|
||||
use crate::pages::repository_fork::RepositoryFork;
|
||||
use crate::pages::repository_protection::RepositoryProtection;
|
||||
use crate::pages::repository_refs::{RepositoryBranches, RepositoryTags};
|
||||
use crate::pages::repository_settings::RepositorySettings;
|
||||
use crate::pages::workspace::WorkspacePage;
|
||||
use crate::pages::workspaces::Workspaces;
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
<Title text="SynCode"/>
|
||||
<Router>
|
||||
<main>
|
||||
<Routes fallback=|| "404 not found">
|
||||
<Route path=StaticSegment("") view=Home/>
|
||||
<Route path=StaticSegment("login") view=Login/>
|
||||
<Route path=StaticSegment("account") view=Account/>
|
||||
<Route path=(StaticSegment("account"), StaticSegment("emails")) view=AccountEmails/>
|
||||
<Route path=(StaticSegment("account"), StaticSegment("security")) view=AccountSecurity/>
|
||||
<Route path=(StaticSegment("org"), ParamSegment("slug"), StaticSegment("teams")) view=OrgTeams/>
|
||||
<Route path=(StaticSegment("org"), ParamSegment("slug"), StaticSegment("agents")) view=OrgAgents/>
|
||||
<Route path=(StaticSegment("repo"), StaticSegment("new")) view=NewRepository/>
|
||||
<Route path=StaticSegment("workspaces") view=Workspaces/>
|
||||
<Route path=(StaticSegment("workspaces"), ParamSegment("id")) view=WorkspacePage/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository")) view=RepositoryPage/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("code")) view=RepositoryCodeDefault/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("code"), ParamSegment("revision")) view=RepositoryCode/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("blob"), ParamSegment("revision")) view=RepositoryBlob/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("blame"), ParamSegment("revision")) view=RepositoryBlame/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("commits")) view=RepositoryCommitsDefault/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("commits"), ParamSegment("revision")) view=RepositoryCommits/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("changeset"), ParamSegment("object_id")) view=RepositoryCommit/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("branches")) view=RepositoryBranches/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("tags")) view=RepositoryTags/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("compare")) view=RepositoryCompare/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("settings")) view=RepositorySettings/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("settings"), StaticSegment("protection")) view=RepositoryProtection/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("fork")) view=RepositoryFork/>
|
||||
// Catch-all profile route — must stay last, or it would
|
||||
// shadow every static route above it (e.g. /login would
|
||||
// match as owner="login").
|
||||
<Route path=ParamSegment("owner") view=Profile/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::{provide_meta_context, Title};
|
||||
use leptos_router::components::{Route, Router, Routes};
|
||||
use leptos_router::{ParamSegment, StaticSegment};
|
||||
|
||||
use crate::pages::account::Account;
|
||||
use crate::pages::account_emails::AccountEmails;
|
||||
use crate::pages::account_security::AccountSecurity;
|
||||
use crate::pages::home::Home;
|
||||
use crate::pages::login::Login;
|
||||
use crate::pages::org_agents::OrgAgents;
|
||||
use crate::pages::org_teams::OrgTeams;
|
||||
use crate::pages::profile::Profile;
|
||||
use crate::pages::repository::{NewRepository, RepositoryPage};
|
||||
use crate::pages::repository_actions::{RepositoryActionRun, RepositoryActions};
|
||||
use crate::pages::repository_code::{
|
||||
RepositoryBlame, RepositoryBlob, RepositoryCode, RepositoryCodeDefault,
|
||||
RepositoryCommitsDefault,
|
||||
};
|
||||
use crate::pages::repository_commits::{RepositoryCommit, RepositoryCommits};
|
||||
use crate::pages::repository_compare::RepositoryCompare;
|
||||
use crate::pages::repository_fork::RepositoryFork;
|
||||
use crate::pages::repository_protection::RepositoryProtection;
|
||||
use crate::pages::repository_refs::{RepositoryBranches, RepositoryTags};
|
||||
use crate::pages::repository_settings::RepositorySettings;
|
||||
use crate::pages::workspace::WorkspacePage;
|
||||
use crate::pages::workspaces::Workspaces;
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
<Title text="SynCode"/>
|
||||
<Router>
|
||||
<main>
|
||||
<Routes fallback=|| "404 not found">
|
||||
<Route path=StaticSegment("") view=Home/>
|
||||
<Route path=StaticSegment("login") view=Login/>
|
||||
<Route path=StaticSegment("account") view=Account/>
|
||||
<Route path=(StaticSegment("account"), StaticSegment("emails")) view=AccountEmails/>
|
||||
<Route path=(StaticSegment("account"), StaticSegment("security")) view=AccountSecurity/>
|
||||
<Route path=(StaticSegment("org"), ParamSegment("slug"), StaticSegment("teams")) view=OrgTeams/>
|
||||
<Route path=(StaticSegment("org"), ParamSegment("slug"), StaticSegment("agents")) view=OrgAgents/>
|
||||
<Route path=(StaticSegment("repo"), StaticSegment("new")) view=NewRepository/>
|
||||
<Route path=StaticSegment("workspaces") view=Workspaces/>
|
||||
<Route path=(StaticSegment("workspaces"), ParamSegment("id")) view=WorkspacePage/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository")) view=RepositoryPage/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("actions")) view=RepositoryActions/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("actions"), ParamSegment("run")) view=RepositoryActionRun/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("code")) view=RepositoryCodeDefault/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("code"), ParamSegment("revision")) view=RepositoryCode/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("blob"), ParamSegment("revision")) view=RepositoryBlob/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("blame"), ParamSegment("revision")) view=RepositoryBlame/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("commits")) view=RepositoryCommitsDefault/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("commits"), ParamSegment("revision")) view=RepositoryCommits/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("changeset"), ParamSegment("object_id")) view=RepositoryCommit/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("branches")) view=RepositoryBranches/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("tags")) view=RepositoryTags/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("compare")) view=RepositoryCompare/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("settings")) view=RepositorySettings/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("settings"), StaticSegment("protection")) view=RepositoryProtection/>
|
||||
<Route path=(ParamSegment("owner"), ParamSegment("repository"), StaticSegment("fork")) view=RepositoryFork/>
|
||||
// Catch-all profile route — must stay last, or it would
|
||||
// shadow every static route above it (e.g. /login would
|
||||
// match as owner="login").
|
||||
<Route path=ParamSegment("owner") view=Profile/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,26 @@
|
||||
mod app;
|
||||
mod branch_protection;
|
||||
mod collab;
|
||||
mod collab_write;
|
||||
mod components;
|
||||
mod graphql;
|
||||
mod heatmap;
|
||||
mod identity;
|
||||
mod org;
|
||||
mod pages;
|
||||
mod platform_agents;
|
||||
mod repo;
|
||||
mod repo_browser;
|
||||
mod repo_insights;
|
||||
mod repo_mutations;
|
||||
mod repo_objects;
|
||||
mod repo_read;
|
||||
|
||||
use app::App;
|
||||
|
||||
fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Warn);
|
||||
leptos::mount::mount_to_body(App);
|
||||
}
|
||||
mod actions;
|
||||
mod app;
|
||||
mod branch_protection;
|
||||
mod collab;
|
||||
mod collab_write;
|
||||
mod components;
|
||||
mod graphql;
|
||||
mod heatmap;
|
||||
mod identity;
|
||||
mod org;
|
||||
mod pages;
|
||||
mod platform_agents;
|
||||
mod repo;
|
||||
mod repo_browser;
|
||||
mod repo_insights;
|
||||
mod repo_mutations;
|
||||
mod repo_objects;
|
||||
mod repo_read;
|
||||
|
||||
use app::App;
|
||||
|
||||
fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Warn);
|
||||
leptos::mount::mount_to_body(App);
|
||||
}
|
||||
@@ -1,21 +1,22 @@
|
||||
pub mod account;
|
||||
pub mod account_emails;
|
||||
pub mod account_security;
|
||||
pub mod home;
|
||||
pub mod login;
|
||||
mod org_agent_forms;
|
||||
pub mod org_agents;
|
||||
pub mod org_teams;
|
||||
pub mod profile;
|
||||
pub mod repository;
|
||||
pub mod repository_code;
|
||||
pub mod repository_commits;
|
||||
pub mod repository_compare;
|
||||
pub mod repository_fork;
|
||||
pub mod repository_protection;
|
||||
pub mod repository_refs;
|
||||
pub mod repository_settings;
|
||||
mod repository_shell;
|
||||
pub mod workspace;
|
||||
mod workspace_forms;
|
||||
pub mod workspaces;
|
||||
pub mod account;
|
||||
pub mod account_emails;
|
||||
pub mod account_security;
|
||||
pub mod home;
|
||||
pub mod login;
|
||||
mod org_agent_forms;
|
||||
pub mod org_agents;
|
||||
pub mod org_teams;
|
||||
pub mod profile;
|
||||
pub mod repository;
|
||||
pub mod repository_actions;
|
||||
pub mod repository_code;
|
||||
pub mod repository_commits;
|
||||
pub mod repository_compare;
|
||||
pub mod repository_fork;
|
||||
pub mod repository_protection;
|
||||
pub mod repository_refs;
|
||||
pub mod repository_settings;
|
||||
mod repository_shell;
|
||||
pub mod workspace;
|
||||
mod workspace_forms;
|
||||
pub mod workspaces;
|
||||
@@ -1,64 +1,65 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::components::A;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
use crate::components::nav::Nav;
|
||||
|
||||
#[component]
|
||||
pub fn RepositoryShell(location: RepositoryLocation, children: Children) -> impl IntoView {
|
||||
let base = location.base();
|
||||
let link =
|
||||
"rounded px-2 py-1 text-sm font-medium text-ink/70 hover:bg-ink/5 hover:text-primary";
|
||||
view! {
|
||||
<div class="flex min-h-screen flex-col bg-paper text-ink">
|
||||
<Nav>
|
||||
<A href="/account" attr:class="text-sm font-medium hover:text-primary">"Account"</A>
|
||||
<A href="/repo/new" attr:class="text-sm font-medium hover:text-primary">"New repository"</A>
|
||||
</Nav>
|
||||
<header class="border-b border-ink/10 bg-white">
|
||||
<div class="mx-auto w-full max-w-6xl px-6 py-5">
|
||||
<p class="text-sm text-ink/55"><span class="font-medium text-ink">{location.owner.clone()}</span>" / "<span class="font-semibold text-ink">{location.repository.clone()}</span></p>
|
||||
<nav class="mt-3 flex flex-wrap gap-1">
|
||||
<A href=base.clone() attr:class=link>"Overview"</A>
|
||||
<A href=format!("{base}/code") attr:class=link>"Code"</A>
|
||||
<A href=format!("{base}/commits") attr:class=link>"Commits"</A>
|
||||
<A href=format!("{base}/branches") attr:class=link>"Branches"</A>
|
||||
<A href=format!("{base}/tags") attr:class=link>"Tags"</A>
|
||||
<A href=format!("{base}/compare") attr:class=link>"Compare"</A>
|
||||
<A href=format!("{base}/settings") attr:class=link>"Settings"</A>
|
||||
<A href=format!("{base}/fork") attr:class=link>"Fork"</A>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main class="mx-auto w-full max-w-6xl flex-1 px-6 py-8">{children()}</main>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RepositoryLocation {
|
||||
pub owner: String,
|
||||
pub repository: String,
|
||||
}
|
||||
|
||||
impl RepositoryLocation {
|
||||
pub fn base(&self) -> String {
|
||||
format!(
|
||||
"/{}/{}",
|
||||
urlencoding::encode(&self.owner),
|
||||
urlencoding::encode(&self.repository)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn repository_location() -> RepositoryLocation {
|
||||
let params = use_params_map().get_untracked();
|
||||
RepositoryLocation {
|
||||
owner: params.get("owner").unwrap_or_default(),
|
||||
repository: params.get("repository").unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_id(value: &str) -> String {
|
||||
value.chars().take(12).collect()
|
||||
}
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::components::A;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
use crate::components::nav::Nav;
|
||||
|
||||
#[component]
|
||||
pub fn RepositoryShell(location: RepositoryLocation, children: Children) -> impl IntoView {
|
||||
let base = location.base();
|
||||
let link =
|
||||
"rounded px-2 py-1 text-sm font-medium text-ink/70 hover:bg-ink/5 hover:text-primary";
|
||||
view! {
|
||||
<div class="flex min-h-screen flex-col bg-paper text-ink">
|
||||
<Nav>
|
||||
<A href="/account" attr:class="text-sm font-medium hover:text-primary">"Account"</A>
|
||||
<A href="/repo/new" attr:class="text-sm font-medium hover:text-primary">"New repository"</A>
|
||||
</Nav>
|
||||
<header class="border-b border-ink/10 bg-white">
|
||||
<div class="mx-auto w-full max-w-6xl px-6 py-5">
|
||||
<p class="text-sm text-ink/55"><span class="font-medium text-ink">{location.owner.clone()}</span>" / "<span class="font-semibold text-ink">{location.repository.clone()}</span></p>
|
||||
<nav class="mt-3 flex flex-wrap gap-1">
|
||||
<A href=base.clone() attr:class=link>"Overview"</A>
|
||||
<A href=format!("{base}/code") attr:class=link>"Code"</A>
|
||||
<A href=format!("{base}/commits") attr:class=link>"Commits"</A>
|
||||
<A href=format!("{base}/branches") attr:class=link>"Branches"</A>
|
||||
<A href=format!("{base}/tags") attr:class=link>"Tags"</A>
|
||||
<A href=format!("{base}/compare") attr:class=link>"Compare"</A>
|
||||
<A href=format!("{base}/actions") attr:class=link>"Actions"</A>
|
||||
<A href=format!("{base}/settings") attr:class=link>"Settings"</A>
|
||||
<A href=format!("{base}/fork") attr:class=link>"Fork"</A>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main class="mx-auto w-full max-w-6xl flex-1 px-6 py-8">{children()}</main>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RepositoryLocation {
|
||||
pub owner: String,
|
||||
pub repository: String,
|
||||
}
|
||||
|
||||
impl RepositoryLocation {
|
||||
pub fn base(&self) -> String {
|
||||
format!(
|
||||
"/{}/{}",
|
||||
urlencoding::encode(&self.owner),
|
||||
urlencoding::encode(&self.repository)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn repository_location() -> RepositoryLocation {
|
||||
let params = use_params_map().get_untracked();
|
||||
RepositoryLocation {
|
||||
owner: params.get("owner").unwrap_or_default(),
|
||||
repository: params.get("repository").unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_id(value: &str) -> String {
|
||||
value.chars().take(12).collect()
|
||||
}
|
||||
@@ -1,0 +1,86 @@
|
||||
use gloo_net::http::Request;
|
||||
use serde::Deserialize;
|
||||
use web_sys::RequestCredentials;
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct ActionRun {
|
||||
pub id: String,
|
||||
pub number: u64,
|
||||
pub commit: String,
|
||||
pub reference: String,
|
||||
pub event: String,
|
||||
pub workflow: String,
|
||||
pub state: String,
|
||||
pub conclusion: String,
|
||||
pub jobs: Vec<ActionJob>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct ActionJob {
|
||||
pub id: String,
|
||||
pub key: String,
|
||||
pub state: String,
|
||||
pub conclusion: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ActionLogs {
|
||||
lines: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn runs(owner: &str, repository: &str) -> Result<Vec<ActionRun>, String> {
|
||||
get(&format!(
|
||||
"{}/control/actions/repositories/{}/{}/runs",
|
||||
control_origin(),
|
||||
urlencoding::encode(owner),
|
||||
urlencoding::encode(repository)
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn run(id: &str) -> Result<ActionRun, String> {
|
||||
get(&format!(
|
||||
"{}/control/actions/runs/{}",
|
||||
control_origin(),
|
||||
urlencoding::encode(id)
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn logs(run: &str, job: &str) -> Result<Vec<String>, String> {
|
||||
let response: ActionLogs = get(&format!(
|
||||
"{}/control/actions/runs/{}/jobs/{}/logs",
|
||||
control_origin(),
|
||||
urlencoding::encode(run),
|
||||
urlencoding::encode(job)
|
||||
))
|
||||
.await?;
|
||||
Ok(response.lines)
|
||||
}
|
||||
|
||||
async fn get<T: serde::de::DeserializeOwned>(url: &str) -> Result<T, String> {
|
||||
let response = Request::get(url)
|
||||
.credentials(RequestCredentials::Include)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
if !response.ok() {
|
||||
return Err(format!("control returned HTTP {}", response.status()));
|
||||
}
|
||||
response.json().await.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn control_origin() -> String {
|
||||
let window = web_sys::window().expect("SynCode front requires a browser window");
|
||||
let hostname = window
|
||||
.location()
|
||||
.hostname()
|
||||
.expect("SynCode front requires a readable browser hostname");
|
||||
if hostname == "localhost" || hostname == "127.0.0.1" {
|
||||
return "https://dev.syncode.sh".to_owned();
|
||||
}
|
||||
match hostname.strip_prefix("new.") {
|
||||
Some(rest) => format!("https://{rest}"),
|
||||
None => format!("https://{hostname}"),
|
||||
}
|
||||
}
|
||||
@@ -1,0 +1,126 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::components::A;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
use crate::actions::{logs, run, runs, ActionJob, ActionRun};
|
||||
|
||||
use super::repository_shell::{repository_location, short_id, RepositoryShell};
|
||||
|
||||
#[component]
|
||||
pub fn RepositoryActions() -> impl IntoView {
|
||||
let location = repository_location();
|
||||
let request = location.clone();
|
||||
let data = LocalResource::new(move || {
|
||||
let owner = request.owner.clone();
|
||||
let repository = request.repository.clone();
|
||||
async move { runs(&owner, &repository).await }
|
||||
});
|
||||
let base = location.base();
|
||||
view! {
|
||||
<RepositoryShell location=location>
|
||||
<h1 class="text-2xl font-semibold">"Actions"</h1>
|
||||
<p class="mt-2 text-sm text-ink/55">"Runs and jobs read directly from SynCode Control."</p>
|
||||
<Suspense fallback=move || view! { <p class="mt-6 text-sm text-ink/50">"Loading runs…"</p> }>
|
||||
{move || {
|
||||
let base = base.clone();
|
||||
Suspend::new(async move {
|
||||
match data.await {
|
||||
Ok(runs) if runs.is_empty() => view! { <p class="mt-6 rounded border border-ink/10 bg-white p-5 text-sm text-ink/55">"No runs yet."</p> }.into_any(),
|
||||
Ok(runs) => run_list(runs, &base),
|
||||
Err(error) => view! { <p class="mt-6 rounded border border-red-200 bg-red-50 p-4 text-sm text-red-700">{error}</p> }.into_any(),
|
||||
}
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
</RepositoryShell>
|
||||
}
|
||||
}
|
||||
|
||||
fn run_list(runs: Vec<ActionRun>, base: &str) -> AnyView {
|
||||
let base = base.to_owned();
|
||||
view! {
|
||||
<ul class="mt-6 divide-y divide-ink/10 overflow-hidden rounded-xl border border-ink/10 bg-white">
|
||||
{runs.into_iter().map(|run| {
|
||||
let status = status(&run.state, &run.conclusion);
|
||||
view! {
|
||||
<li>
|
||||
<A href=format!("{base}/actions/{}", run.id) attr:class="flex flex-col gap-2 px-5 py-4 no-underline hover:bg-ink/5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="font-medium text-ink">{run.workflow}</p>
|
||||
<p class="mt-1 font-mono text-xs text-ink/45">{format!("#{} · {} · {}", run.number, run.event, short_id(&run.commit))}</p>
|
||||
</div>
|
||||
<span class="rounded-full bg-ink/5 px-3 py-1 text-xs font-medium text-ink/65">{status}</span>
|
||||
</A>
|
||||
</li>
|
||||
}
|
||||
}).collect_view()}
|
||||
</ul>
|
||||
}.into_any()
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RepositoryActionRun() -> impl IntoView {
|
||||
let location = repository_location();
|
||||
let run_id = use_params_map()
|
||||
.get_untracked()
|
||||
.get("run")
|
||||
.unwrap_or_default();
|
||||
let request = run_id.clone();
|
||||
let data = LocalResource::new(move || {
|
||||
let id = request.clone();
|
||||
async move { load_run(&id).await }
|
||||
});
|
||||
let actions = format!("{}/actions", location.base());
|
||||
view! {
|
||||
<RepositoryShell location=location>
|
||||
<A href=actions attr:class="text-sm font-medium text-primary no-underline hover:underline">"← Actions"</A>
|
||||
<Suspense fallback=move || view! { <p class="mt-6 text-sm text-ink/50">"Loading run…"</p> }>
|
||||
{move || Suspend::new(async move {
|
||||
match data.await {
|
||||
Ok((run, logs)) => run_detail(run, logs),
|
||||
Err(error) => view! { <p class="mt-6 rounded border border-red-200 bg-red-50 p-4 text-sm text-red-700">{error}</p> }.into_any(),
|
||||
}
|
||||
})}
|
||||
</Suspense>
|
||||
</RepositoryShell>
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_run(id: &str) -> Result<(ActionRun, Vec<(ActionJob, Vec<String>)>), String> {
|
||||
let run = run(id).await?;
|
||||
let mut job_logs = Vec::with_capacity(run.jobs.len());
|
||||
for job in &run.jobs {
|
||||
job_logs.push((job.clone(), logs(&run.id, &job.id).await?));
|
||||
}
|
||||
Ok((run, job_logs))
|
||||
}
|
||||
|
||||
fn run_detail(run: ActionRun, jobs: Vec<(ActionJob, Vec<String>)>) -> AnyView {
|
||||
view! {
|
||||
<div class="mt-5 flex flex-wrap items-start justify-between gap-4">
|
||||
<div><p class="font-mono text-xs text-ink/45">{format!("Run #{}", run.number)}</p><h1 class="mt-1 text-2xl font-semibold">{run.workflow}</h1></div>
|
||||
<span class="rounded-full bg-ink/5 px-3 py-1 text-xs font-medium">{status(&run.state, &run.conclusion)}</span>
|
||||
</div>
|
||||
<dl class="mt-5 grid gap-3 rounded-xl border border-ink/10 bg-white p-5 text-sm sm:grid-cols-3">
|
||||
<div><dt class="text-ink/45">"Commit"</dt><dd class="mt-1 font-mono">{short_id(&run.commit)}</dd></div>
|
||||
<div><dt class="text-ink/45">"Reference"</dt><dd class="mt-1 font-mono">{run.reference}</dd></div>
|
||||
<div><dt class="text-ink/45">"Event"</dt><dd class="mt-1">{run.event}</dd></div>
|
||||
</dl>
|
||||
<div class="mt-6 space-y-4">
|
||||
{jobs.into_iter().map(|(job, lines)| view! {
|
||||
<section class="overflow-hidden rounded-xl border border-ink/10 bg-white">
|
||||
<div class="flex items-center justify-between border-b border-ink/10 px-5 py-3"><h2 class="font-medium">{job.key}</h2><span class="text-xs text-ink/55">{status(&job.state, &job.conclusion)}</span></div>
|
||||
<pre class="max-h-[34rem] overflow-auto bg-ink p-4 text-xs leading-relaxed text-paper">{lines.join("\n")}</pre>
|
||||
</section>
|
||||
}).collect_view()}
|
||||
</div>
|
||||
}.into_any()
|
||||
}
|
||||
|
||||
fn status(state: &str, conclusion: &str) -> String {
|
||||
if conclusion.is_empty() {
|
||||
state.to_owned()
|
||||
} else {
|
||||
format!("{state}/{conclusion}")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user