flams_router_backend/
components.rs

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