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
17pub 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}