From e7a8b7f147fae07f4d73ceb2e7b48366d793e1b1 Mon Sep 17 00:00:00 2001
From: Przemyslaw Olszewski
Date: Sun, 30 Aug 2026 23:06:03 +0200
Subject: [PATCH] feat: Add native Actions views
---
diff --git a/src/app.rs b/src/app.rs
--- a/src/app.rs
+++ b/src/app.rs
@@ -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! {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- // Catch-all profile route — must stay last, or it would
- // shadow every static route above it (e.g. /login would
- // match as owner="login").
-
-
-
-
- }
-}
+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! {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // Catch-all profile route — must stay last, or it would
+ // shadow every static route above it (e.g. /login would
+ // match as owner="login").
+
+
+
+
+ }
+}
diff --git a/src/main.rs b/src/main.rs
--- a/src/main.rs
+++ b/src/main.rs
@@ -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);
+}
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -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;
diff --git a/src/pages/repository_shell.rs b/src/pages/repository_shell.rs
--- a/src/pages/repository_shell.rs
+++ b/src/pages/repository_shell.rs
@@ -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! {
-
-
-
-
{children()}
-
- }
-}
-
-#[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! {
+
+
+
+
{children()}
+
+ }
+}
+
+#[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()
+}
diff --git a/src/actions.rs b/src/actions.rs
--- /dev/null
+++ b/src/actions.rs
@@ -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,
+}
+
+#[derive(Clone, Deserialize)]
+pub struct ActionJob {
+ pub id: String,
+ pub key: String,
+ pub state: String,
+ pub conclusion: String,
+}
+
+#[derive(Deserialize)]
+struct ActionLogs {
+ lines: Vec,
+}
+
+pub async fn runs(owner: &str, repository: &str) -> Result, String> {
+ get(&format!(
+ "{}/control/actions/repositories/{}/{}/runs",
+ control_origin(),
+ urlencoding::encode(owner),
+ urlencoding::encode(repository)
+ ))
+ .await
+}
+
+pub async fn run(id: &str) -> Result {
+ get(&format!(
+ "{}/control/actions/runs/{}",
+ control_origin(),
+ urlencoding::encode(id)
+ ))
+ .await
+}
+
+pub async fn logs(run: &str, job: &str) -> Result, 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(url: &str) -> Result {
+ 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}"),
+ }
+}
diff --git a/src/pages/repository_actions.rs b/src/pages/repository_actions.rs
--- /dev/null
+++ b/src/pages/repository_actions.rs
@@ -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! {
+
+ "Actions"
+ "Runs and jobs read directly from SynCode Control."
+ "Loading runs…"
}>
+ {move || {
+ let base = base.clone();
+ Suspend::new(async move {
+ match data.await {
+ Ok(runs) if runs.is_empty() => view! { "No runs yet."
}.into_any(),
+ Ok(runs) => run_list(runs, &base),
+ Err(error) => view! { {error}
}.into_any(),
+ }
+ })
+ }}
+
+
+ }
+}
+
+fn run_list(runs: Vec, base: &str) -> AnyView {
+ let base = base.to_owned();
+ view! {
+
+ }.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! {
+
+ "← Actions"
+ "Loading run…" }>
+ {move || Suspend::new(async move {
+ match data.await {
+ Ok((run, logs)) => run_detail(run, logs),
+ Err(error) => view! { {error}
}.into_any(),
+ }
+ })}
+
+
+ }
+}
+
+async fn load_run(id: &str) -> Result<(ActionRun, Vec<(ActionJob, Vec)>), 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)>) -> AnyView {
+ view! {
+
+
{format!("Run #{}", run.number)}
{run.workflow}
+
{status(&run.state, &run.conclusion)}
+
+
+ - "Commit"
- {short_id(&run.commit)}
+ - "Reference"
- {run.reference}
+ - "Event"
- {run.event}
+
+
+ {jobs.into_iter().map(|(job, lines)| view! {
+
+ {job.key}
{status(&job.state, &job.conclusion)}
+ {lines.join("\n")}
+
+ }).collect_view()}
+
+ }.into_any()
+}
+
+fn status(state: &str, conclusion: &str) -> String {
+ if conclusion.is_empty() {
+ state.to_owned()
+ } else {
+ format!("{state}/{conclusion}")
+ }
+}
--
SynCode