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