Skip to main content

flams_router_buildqueue_base/
lib.rs

1#![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        //updates:Vec<(String,flams_git::Commit)>
39    },
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        /// #### Errors
56        fn with_queue<R>(
57            &self,
58            id: NonZeroU32,
59            f: impl FnOnce(&flams_system::building::Queue) -> R,
60        ) -> Result<R, String>;
61
62        /// #### Errors
63        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}