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}