flams_router_buildqueue_base/
lib.rs1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3#[cfg(any(
4 all(feature = "ssr", feature = "hydrate", not(feature = "docs-only")),
5 not(any(feature = "ssr", feature = "hydrate"))
6))]
7compile_error!("exactly one of the features \"ssr\" or \"hydrate\" must be enabled");
8
9use flams_router_base::LoginState;
10use flams_utils::unwrap;
11use ftml_dom::utils::css::inject_css;
12use ftml_uris::ArchiveId;
13use std::num::NonZeroU32;
14
15pub mod server_fns;
16
17#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
18pub enum FormatOrTarget {
19 Format(String),
20 Targets(Vec<String>),
21}
22
23#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
24pub struct QueueInfo {
25 pub id: NonZeroU32,
26 pub name: String,
27 pub archives: Option<Vec<RepoInfo>>,
28}
29
30#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
31pub enum RepoInfo {
32 Copy(ArchiveId),
33 Git {
34 id: ArchiveId,
35 remote: String,
36 branch: String,
37 commit: flams_backend_types::git::Commit,
38 },
40}
41
42#[cfg(feature = "ssr")]
43mod login {
44 use std::num::NonZeroU32;
45
46 use flams_router_base::LoginState;
47
48 fn owns(user: &str, queue_name: &str) -> bool {
49 queue_name
50 .strip_prefix(user)
51 .is_some_and(|rest| rest.as_bytes().iter().all(u8::is_ascii_digit))
52 }
53
54 pub trait LoginQueue {
55 fn with_queue<R>(
57 &self,
58 id: NonZeroU32,
59 f: impl FnOnce(&flams_system::building::Queue) -> R,
60 ) -> Result<R, String>;
61
62 fn with_opt_queue<R>(
64 &self,
65 id: Option<NonZeroU32>,
66 f: impl FnOnce(
67 flams_system::building::queue_manager::QueueId,
68 &flams_system::building::Queue,
69 ) -> R,
70 ) -> Result<R, String>;
71 }
72 #[cfg(feature = "ssr")]
73 impl LoginQueue for LoginState {
74 fn with_queue<R>(
75 &self,
76 id: NonZeroU32,
77 f: impl FnOnce(&flams_system::building::Queue) -> R,
78 ) -> Result<R, String> {
79 use flams_system::building::QueueName;
80 let qm = flams_system::building::queue_manager::QueueManager::get();
81 match self {
82 Self::None | Self::Loading => {
83 return Err(format!("Not logged in: {self:?}"));
84 }
85 Self::Admin | Self::NoAccounts | Self::User { is_admin: true, .. } => (),
86 Self::User { name, .. } => {
87 return qm.with_queue(id.into(), move |q| q.map_or_else(
88 || Err(format!("Queue {id} not found")),
89 |q| if matches!(q.name(),QueueName::Sandbox{name:qname,..} if owns(name,&qname))
90 {
91 Ok(f(q))
92 } else {
93 Err(format!("Not allowed to run queue {id}"))
94 }
95 ));
96 }
97 }
98 qm.with_queue(id.into(), move |q| {
99 q.map_or_else(|| Err(format!("Queue {id} not found")), |q| Ok(f(q)))
100 })
101 }
102
103 fn with_opt_queue<R>(
104 &self,
105 id: Option<NonZeroU32>,
106 f: impl FnOnce(
107 flams_system::building::queue_manager::QueueId,
108 &flams_system::building::Queue,
109 ) -> R,
110 ) -> Result<R, String> {
111 use flams_system::building::QueueName;
112 let qm = flams_system::building::queue_manager::QueueManager::get();
113 match (self, id) {
114 (Self::None | Self::Loading, _) =>
115 Err(format!("Not logged in: {self:?}")),
116 (Self::User { name, .. }, Some(id)) => qm.with_queue(id.into(), move |q|
117 q.map_or_else(
118 || Err(format!("Queue {id} not found")),
119 |q| if matches!(q.name(),QueueName::Sandbox{name:qname,..} if owns(name,&qname))
120 {
121 Ok(f(id.into(), q))
122 } else {
123 Err(format!("Not allowed to run queue {id}"))
124 }
125 )),
126 (Self::Admin, Some(id)) => qm.with_queue(id.into(), |q|
127 q.map_or_else(
128 || Err(format!("Queue {id} not found")),
129 |q| Ok(f(id.into(), q))
130 )),
131 (Self::User { name, .. }, _) => {
132 let queue = qm.new_queue(name);
133 qm.with_queue(queue, |q| {
134 let Some(q) = q else { unreachable!() };
135 Ok(f(queue, q))
136 })
137 }
138 (Self::Admin, _) => {
139 let queue = qm.new_queue("admin");
140 qm.with_queue(queue, |q| {
141 let Some(q) = q else { unreachable!() };
142 Ok(f(queue, q))
143 })
144 }
145 (Self::NoAccounts, _) => qm.with_global(|q| {
146 Ok(f(
147 flams_system::building::queue_manager::QueueId::global(),
148 q,
149 ))
150 }),
151 }
152 }
153 }
154}
155#[cfg(feature = "ssr")]
156pub use login::*;
157
158use leptos::prelude::*;
159#[must_use]
160pub fn select_queue(queue_id: RwSignal<Option<NonZeroU32>>) -> impl IntoView {
161 use flams_web_utils::components::display_error;
162 use ftml_component_utils::Spinner;
163 move || {
164 let user = LoginState::get();
165 if matches!(user, LoginState::NoAccounts) {
166 return None;
167 }
168 let r = Resource::new(|| (), move |()| server_fns::get_queues());
169 Some(view! {<Suspense fallback = || view!(<Spinner/>)>{move || {
170 match r.get() {
171 None => leptos::either::EitherOf3::A(view!(<Spinner/>)),
172 Some(Err(e)) => leptos::either::EitherOf3::B(display_error(e.to_string().into())),
173 Some(Ok(queues)) => leptos::either::EitherOf3::C(view!{<div><div style="width:fit-content;margin-left:auto;">{
174 do_queues(queue_id,queues)
175 }</div></div>})
176 }
177 }}</Suspense>})
178 }
179}
180
181fn do_queues(queue_id: RwSignal<Option<NonZeroU32>>, v: Vec<QueueInfo>) -> impl IntoView {
182 use ftml_component_utils::Select;
183 inject_css("flams-select-queue", include_str!("select_queue.css"));
184 let queues = if v.is_empty() {
185 vec![(0u32, "New Build Queue".to_string())]
186 } else {
187 v.into_iter()
188 .map(|q| (q.id.get(), q.name))
189 .chain(std::iter::once((0u32, "New Build Queue".to_string())))
190 .collect()
191 };
192 let value = RwSignal::new(unwrap!(queues.first()).clone().1);
193 let qc = queues.clone();
194 let _ = Effect::new(move |_| {
195 let queue = value.get();
196 if queue == "New Build Queue" {
197 queue_id.update_untracked(|v| *v = None);
198 } else {
199 let idx = unwrap!(? qc.iter().find_map(|(id,name)| if *name == queue {Some(*id)} else {None}));
200 queue_id.update_untracked(|v| *v = Some(unwrap!(NonZeroU32::new(idx))));
201 }
202 });
203 view! {
204 <span style="font-style:italic;">"Build Queue: "
205 <Select value class="flams-select-queue">{
206 queues.into_iter().map(|(_,name)| view!{
207 <option value=name>{name.clone()}</option>
208 }).collect_view()
209 }</Select></span>
210 }
211}