flams_router_git_components/
lib.rs

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