Skip to main content

flams_router_backend/
components.rs

1use crate::FileStates;
2use flams_backend_types::archives::{ArchiveData, ArchiveGroupData, DirectoryData, FileData};
3use flams_router_base::{LoginState, maybe_lazy};
4use flams_router_buildqueue_base::{FormatOrTarget, select_queue, server_fns::enqueue};
5use flams_utils::unwrap;
6use flams_web_utils::components::{
7    Header, LazySubtree, Leaf, Subtree, Tree, message_action, wait_and_then, wait_and_then_fn,
8};
9use ftml_dom::utils::css::inject_css;
10use ftml_ontology::utils::time::Timestamp;
11use ftml_uris::ArchiveId;
12use leptos::prelude::*;
13use std::num::NonZeroU32;
14
15maybe_lazy!(ArchivesTop = archives_top());
16
17//#[component]
18pub fn archives_top() -> AnyView {
19    wait_and_then_fn(
20        || super::server_fns::group_entries(None),
21        |(groups, archives)| {
22            {
23                let mut summary = flams_backend_types::archives::FileStateSummary::default();
24                for g in &groups {
25                    if let Some(s) = g.summary {
26                        summary.merge(s);
27                    }
28                }
29                for a in &archives {
30                    if let Some(s) = a.summary {
31                        summary.merge(s);
32                    }
33                }
34                view!(<Tree><Subtree expanded=true>
35            <Header slot>
36                "All Archives "
37                {badge(summary)}
38                {dialog(move |signal| if signal.get() {
39                  Some(wait_and_then(
40                    move || super::server_fns::build_status(None,None),
41                    move |state| modal(None,None,state,None)
42                  ))
43                } else {None})}
44            </Header>
45            <ArchivesAndGroups archives groups/>
46        </Subtree></Tree>)
47            }
48            .into_any()
49        },
50    )
51    .into_any()
52}
53
54#[component]
55fn ArchivesAndGroups(groups: Vec<ArchiveGroupData>, archives: Vec<ArchiveData>) -> AnyView {
56    view! {
57      {groups.into_iter().map(group).collect_view()}
58      {archives.into_iter().map(archive).collect_view()}
59    }
60    .into_any()
61}
62
63fn group(a: ArchiveGroupData) -> AnyView {
64    let id = a.id.clone();
65    let header = view!(
66      <ftml_component_utils::icons::LibraryIcon/>" "
67      {a.id.last().to_string()}
68      {a.summary.map(badge)}
69      {dialog(move |signal| if signal.get() {
70        let id = id.clone();
71        let title = id.clone();
72        Some(wait_and_then(
73          move || super::server_fns::build_status(Some(id.clone()),None),
74          move |state| modal(Some(title),None,state,None)
75        ))
76      } else {None})}
77    );
78    let id = a.id;
79    let f = move || super::server_fns::group_entries(Some(id.clone()));
80    view! {
81      <LazySubtree>
82        <Header slot>{header}</Header>
83        {
84          wait_and_then(f.clone(),
85          |(groups,archives)|
86            view!(<Tree><ArchivesAndGroups groups archives/></Tree>).into_any()
87          )
88        }
89      </LazySubtree>
90    }
91    .into_any()
92}
93
94fn archive(a: ArchiveData) -> AnyView {
95    let id = a.id.clone();
96    let header = view!(
97      <ftml_component_utils::icons::ClosedBookIcon/>" "
98      {a.id.last().to_string()}
99      {a.summary.map(badge)}
100      {dialog(move |signal| if signal.get() {
101        let id = id.clone();
102        let title = id.clone();
103        Some(wait_and_then(
104          move || super::server_fns::build_status(Some(id.clone()),None),
105          move |state| modal(Some(title),None,state,None)
106        ))
107      } else {None})}
108    );
109    let id = a.id;
110    view! {
111      <LazySubtree>
112        <Header slot>{header}</Header>
113        {
114          let id = id.clone();
115          let nid = id.clone();
116          wait_and_then(move || super::server_fns::archive_entries(id.clone(),None),move |(dirs,files)|
117            view!(<Tree>{dirs_and_files(&nid,dirs,files)}</Tree>).into_any()
118          )
119        }
120      </LazySubtree>
121    }.into_any()
122}
123
124fn dirs_and_files(archive: &ArchiveId, dirs: Vec<DirectoryData>, files: Vec<FileData>) -> AnyView {
125    view! {
126      {dirs.into_iter().map(|d| dir(archive.clone(),d)).collect_view()}
127      {files.into_iter().map(|f| file(archive.clone(),f)).collect_view()}
128    }
129    .into_any()
130}
131
132fn dir(archive: ArchiveId, d: DirectoryData) -> AnyView {
133    let pathstr = unwrap!(d.rel_path.split('/').last()).to_string();
134    let id = archive.clone();
135    let rel_path = d.rel_path.clone();
136    let header = view!(
137      <ftml_component_utils::icons::FolderIcon/>" "
138      {pathstr}
139      {d.summary.map(badge)}
140      {dialog(move |signal| if signal.get() {
141        let id = id.clone();
142        let title = id.clone();
143        let rel_path = rel_path.clone();
144        Some(wait_and_then(
145          move || super::server_fns::build_status(Some(id.clone()),None),
146          move |state| modal(Some(title),Some(rel_path),state,None)
147        ))
148      } else {None})}
149    );
150    let id = archive.clone();
151    let rel_path = d.rel_path;
152    let f = move || super::server_fns::archive_entries(id.clone(), Some(rel_path.clone()));
153    view! {
154      <LazySubtree>
155        <Header slot>{header}</Header>
156        {
157          let archive = archive.clone();
158          wait_and_then(
159              f.clone(),
160              move |(dirs,files)|
161            view!(<Tree>{dirs_and_files(&archive,dirs,files)}</Tree>).into_any()
162          )
163        }
164      </LazySubtree>
165    }
166    .into_any()
167}
168
169fn file(archive: ArchiveId, f: FileData) -> AnyView {
170    use flams_web_utils::components::{Drawer, Header, Trigger};
171    use ftml_component_utils::{Button, ButtonAppearance};
172
173    let link = format!("/?a={archive}&rp={}", f.rel_path);
174    let button = format!("[{archive}]/{}", f.rel_path);
175    let comps = ftml_uris::components::DocumentUriComponents::RelPath {
176        a: archive.clone(),
177        rp: f.rel_path.clone(),
178    };
179
180    let pathstr = unwrap!(f.rel_path.split('/').next_back()).to_string();
181    let header = view!(
182      <Drawer lazy=true>
183        <Trigger slot>
184          <ftml_component_utils::icons::FileIcon/>" "
185          {pathstr}
186        </Trigger>
187        <Header slot><a href=link target="_blank">
188          <Button appearance=ButtonAppearance::Subtle>{button}</Button>
189        </a></Header>
190        <div style="width:min-content"><flams_router_content::components::Document doc=comps.clone()/></div>
191      </Drawer>
192      {dialog(move |signal| if signal.get() {
193
194        let id = archive.clone();
195        let rel_path = f.rel_path.clone();
196        let title = archive.clone();
197        let rp = rel_path.clone();
198        let fmt = f.format.clone();
199        Some(wait_and_then_fn(
200          move || super::server_fns::build_status(Some(id.clone()),Some(rp.clone())),
201          move |state| modal(Some(title.clone()),Some(rel_path.clone()),state,Some(fmt.clone()))
202        ))
203      } else {None})}
204    );
205    view! {
206      <Leaf>{header}</Leaf>
207    }
208    .into_any()
209}
210
211fn badge(state: crate::FileStateSummary) -> AnyView {
212    use ftml_component_utils::{Badge, BadgeAppearance, BadgeColor};
213    view! {
214      {if state.new == 0 {None} else {Some(view!(
215        " "<Badge class="flams-mathhub-badge" appearance=BadgeAppearance::Outline color=BadgeColor::Success>{state.new}</Badge>
216      ))}}
217      {if state.stale == 0 {None} else {Some(view!(
218        " "<Badge class="flams-mathhub-badge" appearance=BadgeAppearance::Outline color=BadgeColor::Warning>{state.stale}</Badge>
219      ))}}
220      {if state.deleted == 0 {None} else {Some(view!(
221        " "<Badge class="flams-mathhub-badge" appearance=BadgeAppearance::Outline color=BadgeColor::Danger>{state.deleted}</Badge>
222      ))}}
223    }.into_any()
224}
225
226fn dialog<V: IntoView + 'static>(
227    children: impl Fn(RwSignal<bool>) -> V + Send + Clone + 'static,
228) -> AnyView {
229    use ftml_component_utils::{Dialog, DialogBody, DialogContent, DialogSurface};
230    let clicked = RwSignal::new(false);
231    (move || {
232        if matches!(
233            LoginState::get(),
234            LoginState::Admin | LoginState::NoAccounts | LoginState::User { is_admin: true, .. }
235        ) {
236            let children = (children.clone())(clicked);
237            Some(view! {
238              <Dialog open=clicked><DialogSurface><DialogBody><DialogContent>
239              {children}
240              </DialogContent></DialogBody></DialogSurface></Dialog>
241              <span on:click=move |_| {clicked.set(true)} style="cursor: help;">
242                "🛈"
243              </span>
244            })
245        } else {
246            None
247        }
248    })
249    .into_any()
250}
251
252fn modal(
253    archive: Option<ArchiveId>,
254    path: Option<String>,
255    states: FileStates,
256    format: Option<String>,
257) -> AnyView {
258    use ftml_component_utils::{
259        Block, BoldCaption, Button, ButtonSize, Divider, Header, HeaderRight, Table, TableCell,
260        TableHeader, TableRow,
261    };
262    let do_clean = path.is_none();
263    let title = path.as_ref().map_or_else(
264        || {
265            archive
266                .as_ref()
267                .map_or_else(|| "All Archives".to_string(), ArchiveId::to_string)
268        },
269        |path| format!("[{}]{path}", archive.as_ref().expect("unreachable")),
270    );
271    let targets = format.is_some();
272    let queue_id = RwSignal::<Option<NonZeroU32>>::new(None);
273    let act = message_action(
274        move |(t, b, clean)| {
275            enqueue(
276                archive.clone(),
277                t,
278                path.clone(),
279                Some(b),
280                queue_id.get_untracked(),
281                clean,
282            )
283        },
284        |i| format!("{i} new build tasks queued"),
285    );
286    let clean_btn = move |f: String| {
287        if do_clean {
288            Some(view! {
289                <Button size=ButtonSize::Small on_click=move |_|
290                {act.dispatch((FormatOrTarget::Format(f.clone()),false,true));}
291                >"clean"</Button>
292            })
293        } else {
294            None
295        }
296    };
297    view! {
298      <div style="text-align:left"><Block>
299        <HeaderRight slot>{format.map(|f| {
300            let f2 = f.clone();
301            let f3 = f.clone();
302            view!{
303            <Button size=ButtonSize::Small on_click=move |_|
304                {act.dispatch((FormatOrTarget::Format(f.clone()),true,false));}
305            >"stale"</Button>
306            <Button size=ButtonSize::Small on_click=move |_|
307                {act.dispatch((FormatOrTarget::Format(f2.clone()),false,false));}
308            >"all"</Button>
309            {clean_btn(f3)}
310            }
311        })}</HeaderRight>
312          <Header slot>
313            <BoldCaption>{title}</BoldCaption>
314          </Header>
315          <Divider/>
316          {select_queue(queue_id)}
317          <Table>
318              <TableHeader slot>
319                    <TableCell><BoldCaption>{if targets {"Target"} else {"Format"}}</BoldCaption></TableCell>
320                    <TableCell><BoldCaption>"New"</BoldCaption></TableCell>
321                    <TableCell><BoldCaption>"Stale"</BoldCaption></TableCell>
322                    <TableCell><BoldCaption>"Up to date"</BoldCaption></TableCell>
323                    <TableCell><BoldCaption>"Last built"</BoldCaption></TableCell>
324                    <TableCell><BoldCaption>"Last changed"</BoldCaption></TableCell>
325                    <TableCell><BoldCaption>"Build"</BoldCaption></TableCell>
326              </TableHeader>
327              {states.0.into_iter().map(|(name,summary)| {
328                let fmt1 = name.clone();
329                let fmt2 = name.clone();
330                let fmt3 = name.clone();
331                view!{
332                  <TableRow>
333                    <TableCell><BoldCaption>{name}</BoldCaption></TableCell>
334                    <TableCell>{summary.new}</TableCell>
335                    <TableCell>{summary.stale}</TableCell>
336                    <TableCell>{summary.up_to_date}</TableCell>
337                    <TableCell>{if summary.last_built == Timestamp::zero() {"(Never)".to_string()} else {summary.last_built.to_string()}}</TableCell>
338                    <TableCell>{if summary.last_changed == Timestamp::zero() {"(Never)".to_string()} else {summary.last_changed.to_string()}}</TableCell>
339                    <TableCell><div style="display:flex;flex-direction:column;">
340                      <Button size=ButtonSize::Small on_click=move |_|
341                        {act.dispatch((if targets {todo!()} else {
342                          FormatOrTarget::Format(fmt1.clone())
343                        },true,false));}
344                      >"stale"</Button>
345                      <Button size=ButtonSize::Small on_click=move |_|
346                        {act.dispatch((if targets {todo!()} else {
347                          FormatOrTarget::Format(fmt2.clone())
348                        },false,false));}
349                      >"all"</Button>
350                      {clean_btn(fmt3)}
351                    </div></TableCell>
352                  </TableRow>
353                }
354            }).collect_view()}
355        </Table>
356          </Block>
357          </div>
358    }.into_any()
359}