flams_router_backend/
components.rs

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