Skip to main content

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