flams_router_git_components/
lib.rs

1#![recursion_limit = "256"]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![allow(clippy::must_use_candidate)]
4
5#[cfg(any(
6    all(feature = "ssr", feature = "hydrate", not(feature = "docs-only")),
7    not(any(feature = "ssr", feature = "hydrate"))
8))]
9compile_error!("exactly one of the features \"ssr\" or \"hydrate\" must be enabled");
10
11use flams_router_buildqueue_base::select_queue;
12use flams_router_git_base::{
13    GitState,
14    server_fns::{clone_to_queue, get_archives, get_branches, update_from_branch},
15};
16use flams_utils::vecmap::VecMap;
17use flams_web_utils::components::{Spinner, display_error};
18use ftml_uris::ArchiveId;
19use leptos::{
20    either::{Either, EitherOf4},
21    prelude::*,
22};
23use std::num::NonZeroU32;
24
25#[component]
26pub fn Archives() -> impl IntoView {
27    let r = Resource::new(|| (), |()| get_archives());
28    view! {<Suspense fallback = || view!(<Spinner/>)>{move ||
29      match r.get() {
30        Some(Ok(projects)) if projects.is_empty() => EitherOf4::A("(No archives)"),
31        Some(Err(e)) => EitherOf4::B(
32          display_error(e.to_string().into())
33        ),
34        None => EitherOf4::C(view!(<Spinner/>)),
35        Some(Ok(projects)) => EitherOf4::D(do_projects(projects))
36      }
37    }</Suspense>}
38}
39
40#[derive(Debug, Copy, Clone)]
41struct QueueSignal(RwSignal<Option<NonZeroU32>>, RwSignal<()>);
42#[derive(Debug, Clone, Default)]
43struct ProjectTree {
44    pub children: Vec<Either<Project, ProjectGroup>>,
45}
46
47#[derive(Debug, Clone)]
48struct Project {
49    pub id: u64,
50    pub name: ArchiveId,
51    pub url: String,
52    pub path: String,
53    pub state: RwSignal<GitState>,
54    pub default_branch: Option<String>,
55}
56impl Eq for Project {}
57impl PartialEq for Project {
58    #[inline]
59    fn eq(&self, other: &Self) -> bool {
60        self.id == other.id
61    }
62}
63
64#[derive(Debug, Clone)]
65struct ProjectGroup {
66    pub name: String,
67    pub children: ProjectTree,
68}
69
70impl ProjectTree {
71    #[allow(clippy::redundant_else)]
72    #[allow(clippy::needless_pass_by_value)]
73    fn add(&mut self, repo: flams_backend_types::git::Project, id: ArchiveId, state: GitState) {
74        let mut steps = id.steps().enumerate().peekable();
75        let mut current = self;
76        while let Some((i, step)) = steps.next() {
77            macro_rules! insert {
78                ($j:ident) => {
79                    if steps.peek().is_none() {
80                        current.children.insert(
81                            $j,
82                            Either::Left(Project {
83                                url: repo.url,
84                                id: repo.id,
85                                path: repo.path,
86                                name: id.clone(),
87                                default_branch: repo.default_branch,
88                                state: RwSignal::new(state),
89                            }),
90                        );
91                        return;
92                    } else {
93                        current.children.insert(
94                            $j,
95                            Either::Right(ProjectGroup {
96                                name: step.to_string(),
97                                children: ProjectTree::default(),
98                            }),
99                        );
100                        let Either::Right(e) = &mut current.children[$j] else {
101                            unreachable!()
102                        };
103                        current = &mut e.children;
104                    }
105                };
106            }
107            match current.children.binary_search_by_key(&step, |e| match e {
108                Either::Left(p) => p.name.steps().nth(i).unwrap_or_else(|| unreachable!()),
109                Either::Right(g) => &g.name,
110            }) {
111                Err(j) => insert!(j),
112                Ok(j) => {
113                    let cont = match &current.children[j] {
114                        Either::Left(_) => false,
115                        Either::Right(_) => true,
116                    };
117                    if cont {
118                        let Either::Right(e) = &mut current.children[j] else {
119                            unreachable!()
120                        };
121                        current = &mut e.children;
122                    } else {
123                        insert!(j);
124                    }
125                }
126            }
127        }
128    }
129}
130
131fn do_projects(
132    vec: Vec<(flams_backend_types::git::Project, ArchiveId, GitState)>,
133) -> impl IntoView {
134    use flams_web_utils::components::{Header, Leaf, Subtree, Tree};
135    use thaw::Caption1Strong;
136    fn inner_tree(tree: ProjectTree) -> impl IntoView {
137        tree.children.into_iter().map(|c| match c {
138        Either::Left(project) => Either::Left(view!{<Leaf><div>{move || project.state.with(|state| {
139          if matches!(state,GitState::None){
140            let state = project.state;
141            Either::Right(unmanaged(project.name.clone(),project.id,state,project.path.clone(),project.url.clone()))
142          } else {
143            Either::Left(managed(project.name.clone(),project.id,state,project.default_branch.clone(),project.path.clone(),project.url.clone(),project.state))
144          }
145        })
146      }</div></Leaf>}),
147      Either::Right(group) => {
148        Either::Right(view!{
149          <Subtree><Header slot><div>{group.name}</div></Header>{inner_tree(group.children)}</Subtree>
150        }.into_any())
151      }
152    }).collect_view()
153    }
154
155    let queue = RwSignal::new(None);
156    let get_queues = RwSignal::new(());
157    provide_context(QueueSignal(queue, get_queues));
158
159    let mut tree = ProjectTree::default();
160    for (p, id, state) in vec {
161        tree.add(p, id, state);
162    }
163
164    view! {
165      <Caption1Strong>"Archives on GitLab"</Caption1Strong>
166      {move || {get_queues.get(); select_queue(queue)}}
167      <Tree>{inner_tree(tree)}</Tree>
168    }
169}
170
171#[allow(clippy::needless_pass_by_value)]
172fn managed(
173    name: ArchiveId,
174    _id: u64,
175    state: &GitState,
176    default_branch: Option<String>,
177    path: String,
178    git_url: String,
179    and_then: RwSignal<GitState>,
180) -> impl IntoView + use<> {
181    use thaw::{Button, ButtonSize, Combobox, ComboboxOption};
182    match state {
183        GitState::Queued { commit, .. } => leptos::either::EitherOf3::A(view! {
184          {path}
185          " (commit "{commit[..8].to_string()}" currently queued)"
186        }),
187        GitState::Live { commit, updates } if updates.is_empty() => {
188            leptos::either::EitherOf3::B(view! {
189              {path}
190              " (commit "{commit[..8].to_string()}" up to date)"
191            })
192        }
193        GitState::Live { commit, updates } => leptos::either::EitherOf3::C({
194            let mut updates = updates.clone();
195            if let Some(branch) = default_branch {
196                if let Some(main) = updates.iter().position(|(b, _)| b == &branch) {
197                    let main = updates.remove(main);
198                    updates.insert(0, main);
199                }
200            }
201            let first = updates
202                .first()
203                .map(|(name, _)| name.clone())
204                .unwrap_or_default();
205            let branch = RwSignal::new(first.clone());
206            let _ = Effect::new(move || {
207                if branch.with(String::is_empty) {
208                    branch.set(first.clone());
209                }
210            });
211            let QueueSignal(queue, get_queues) = expect_context();
212            let commit_map: VecMap<_, _> = updates.clone().into();
213            let namecl = name;
214            let (act, v) = flams_web_utils::components::waiting_message_action(
215                move |()| {
216                    update_from_branch(
217                        queue.get_untracked(),
218                        namecl.clone(),
219                        git_url.clone(),
220                        branch.get_untracked(),
221                    )
222                },
223                move |(i, q)| {
224                    let commit = commit_map
225                        .get(&branch.get_untracked())
226                        .unwrap_or_else(|| unreachable!())
227                        .clone();
228                    get_queues.set(());
229                    and_then.set(GitState::Queued {
230                        commit: commit.id,
231                        queue: q,
232                    });
233                    format!("{i} jobs queued")
234                },
235            );
236
237            view! {
238              {v}
239              <span style="color:green">{path}
240                " (commit "{commit[..8].to_string()}") Updates available: "
241              </span>
242              <div style="margin-left:10px">
243                <Button size=ButtonSize::Small on_click=move |_| {act.dispatch(());}>"Update"</Button>
244                " from branch: "
245                <div style="display:inline-block;"><Combobox value=branch>{
246                  updates.into_iter().map(|(name,commit)| {let vname = name.clone(); view!{
247                    <ComboboxOption text=vname.clone() value=vname>
248                      {name}<span style="font-size:x-small">" (Last commit "{commit.id[..8].to_string()}" at "{commit.created_at.to_string()}" by "{commit.author_name}")"</span>
249                    </ComboboxOption>
250                  }}).collect_view()
251                }</Combobox></div>
252              </div>
253            }
254        }),
255        GitState::None => unreachable!(),
256    }
257}
258
259fn unmanaged(
260    name: ArchiveId,
261    id: u64,
262    and_then: RwSignal<GitState>,
263    path: String,
264    git_url: String,
265) -> impl IntoView {
266    use thaw::{Button, ButtonSize, Combobox, ComboboxOption};
267    let r = Resource::new(
268        || (),
269        move |()| async move {
270            get_branches(id).await.map(|mut branches| {
271                let main = branches.iter().position(|b| b.default);
272                let main = main.map(|i| branches.remove(i));
273                if let Some(b) = main {
274                    branches.insert(0, b);
275                }
276                let release = branches.iter().position(|b| b.name == "release");
277                let release = release.map(|i| branches.remove(i));
278                (branches, release.is_some())
279            })
280        },
281    );
282    view! {
283      <span style="color:grey">{path}" (unmanaged) "</span>
284      <Suspense fallback=|| view!(<flams_web_utils::components::Spinner/>)>{move ||
285        match r.get() {
286          Some(Err(e)) => leptos::either::EitherOf3::B(flams_web_utils::components::display_error(e.to_string().into())),
287          None => leptos::either::EitherOf3::C(view!(<flams_web_utils::components::Spinner/>)),
288          Some(Ok((branches,has_release))) => leptos::either::EitherOf3::A({
289            let first = branches.first().map(|f| f.name.clone()).unwrap_or_default();
290            let branch = RwSignal::new(first.clone());
291            let _ = Effect::new(move || if branch.with(String::is_empty) {
292              branch.set(first.clone());
293            });
294            let QueueSignal(queue,get_queues) = expect_context();
295            let name = name.clone();
296            let git_url = git_url.clone();
297            let commit_map : VecMap<_,_> = branches.iter().map(|b| (b.name.clone(),b.commit.clone())).collect();
298            let (act,v) = flams_web_utils::components::waiting_message_action(
299              move |()| clone_to_queue(queue.get_untracked(),name.clone(),git_url.clone(),branch.get_untracked(),has_release),
300              move |(i,q)| {
301                let commit = commit_map.get(&branch.get_untracked()).unwrap_or_else(|| unreachable!()).clone();
302                get_queues.set(());
303                and_then.set(GitState::Queued{commit:commit.id,queue:q});
304                format!("{i} jobs queued")
305              }
306            );
307            view!{<div style="margin-left:10px">{v}
308            <Button size=ButtonSize::Small on_click=move |_| {act.dispatch(());}>"Add"</Button>
309              " from branch: "<div style="display:inline-block;"><Combobox value=branch>{
310                branches.into_iter().map(|b| {let name = b.name.clone(); view!{
311                  <ComboboxOption value=name.clone() text=name>
312                    {b.name}<span style="font-size:x-small">" (Last commit "{b.commit.id[..8].to_string()}" at "{b.commit.created_at.to_string()}" by "{b.commit.author_name}")"</span>
313                  </ComboboxOption>
314                }}).collect_view()
315              }</Combobox></div></div>
316            }
317          })
318        }
319      }</Suspense>
320    }
321}