Skip to main content

flams_router_dashboard/
lib.rs

1#![recursion_limit = "512"]
2#![allow(clippy::must_use_candidate)]
3
4/*
5#[cfg(all(feature = "ssr", feature = "hydrate", not(feature = "docs-only")))]
6compile_error!("exactly one of the features \"ssr\" or \"hydrate\" must be enabled");
7
8#[cfg(not(any(feature = "ssr", feature = "hydrate")))]
9compile_error!("exactly one of the features \"ssr\" or \"hydrate\" must be enabled");
10 */
11
12pub mod query;
13mod settings;
14
15pub mod ws {
16    /*pub use flams_flodown::math::MathSocket;
17    #[cfg(feature = "ssr")]
18    pub use flams_flodown::math::TeXSocket;*/
19    pub use flams_router_base::ws::*;
20    pub use flams_router_buildqueue_components::QueueSocket;
21    pub use flams_router_logging::LogSocket;
22}
23
24pub mod server_fns {
25    pub mod content {
26        pub use flams_router_content::server_fns::*;
27    }
28    pub mod backend {
29        pub use flams_router_backend::server_fns::*;
30    }
31    pub mod buildqueue {
32        pub use flams_router_buildqueue_base::server_fns::*;
33    }
34    pub mod git {
35        pub use flams_router_git_base::server_fns::*;
36    }
37    pub mod login {
38        pub use flams_router_login::server_fns::*;
39    }
40    pub mod search {
41        pub use flams_router_search::{search_query, search_symbols};
42    }
43    pub use super::query::query_api as query;
44    pub use super::settings::{get_settings as settings, reload};
45}
46
47pub use flams_router_base::LoginState;
48use flams_router_base::maybe_lazy;
49use flams_web_utils::components::{Layout, LayoutHeader, LayoutSider, wait_and_then_fn};
50use ftml_component_utils::{Divider, Grid, GridItem, inject_css_after};
51use ftml_components::config::AllowSubterms;
52use ftml_dom::FtmlViews;
53use ftml_uris::DocumentUri;
54use leptos::prelude::*;
55use leptos_meta::{Title, provide_meta_context};
56use leptos_router::{
57    components::{Outlet, ParentRoute, Redirect, Route, Router, Routes},
58    path,
59};
60
61#[component]
62pub fn Main() -> AnyView {
63    provide_meta_context();
64    let (_, set_is_routing) = signal(false);
65    view! {
66        <Title text="𝖥𝖫∀𝖬∫"/>
67        <Router set_is_routing>{
68            //let has_params = Memo::new(move |_| use_query_map().with(|p| p.get_str("a").is_some() || p.get_str("uri").is_some()));
69            view!{<Routes fallback=|| NotFound()>
70                <ParentRoute/* ssr=SsrMode::InOrder*/ path=() view=Top>
71                    <ParentRoute path=path!("/dashboard") view=Dashboard>
72                        <ParentRoute path=path!("mathhub") view={|| main_page(Page::MathHub)}>
73                            <Route path=path!("") view={maybe_lazy!(flams_router_backend::components::ArchivesTop)}/>
74                        </ParentRoute>
75                        //<Route path="graphs" view=|| view!(<MainPage page=Page::Graphs/>)/>
76                        <ParentRoute path=path!("log") view={|| main_page(Page::Log)}>
77                            <Route path=path!("") view={maybe_lazy!(flams_router_logging::Logger)}/>
78                        </ParentRoute>
79                        <ParentRoute path=path!("queue") view={|| main_page(Page::Queue)}>
80                            <Route path=path!("") view={maybe_lazy!(flams_router_buildqueue_components::QueuesTop)}/>
81                        </ParentRoute>
82                        <ParentRoute path=path!("settings") view={|| main_page(Page::Settings)}>
83                            <Route path=path!("") view={maybe_lazy!(settings::Settings)}/>
84                        </ParentRoute>
85                        <ParentRoute path=path!("query") view={|| main_page(Page::Query)}>
86                            <Route path=path!("") view={maybe_lazy!(query::Query)}/>
87                        </ParentRoute>
88                        <ParentRoute path=path!("archives") view={|| main_page(Page::MyArchives)}>
89                            <Route path=path!("") view={maybe_lazy!(flams_router_git_components::Archives)}/>
90                        </ParentRoute>
91                        <ParentRoute path=path!("users") view={|| main_page(Page::Users)}>
92                            <Route path=path!("") view={maybe_lazy!(flams_router_login::components::Users)}/>
93                        </ParentRoute>
94                        <ParentRoute path=path!("search") view={|| main_page(Page::Search)}>
95                            <Route path=path!("") view={maybe_lazy!(flams_router_search::components::SearchTop)}/>
96                        </ParentRoute>
97                        /*<ParentRoute path=path!("flodown") view={|| main_page(Page::FloDown)}>
98                            <Route path=path!("") view={maybe_lazy!(flams_flodown::FloDownEditor)}/>
99                        </ParentRoute>*/
100                        <ParentRoute path=path!("") view={|| main_page(Page::Home)}>
101                            <Route path=path!("") view={maybe_lazy!(Home)}/>
102                        </ParentRoute>
103                        <ParentRoute path=path!("*any") view={|| main_page(Page::NotFound)}>
104                            <Route path=path!("") view=||view!(<NotFound/>)/>
105                        </ParentRoute>
106                    </ParentRoute>
107                    <ParentRoute path=path!("/vscode") view={maybe_lazy!(flams_router_vscode::VSCWrap)}>// flams_router_vscode::VSCodeWrap>
108                        <Route path=path!("search") view={maybe_lazy!(flams_router_search::vscode::VSCSearch)}/>
109                        <Route path=path!("proofs") view={maybe_lazy!(flams_router_vscode::checks::Checks)}/>
110                    </ParentRoute>
111                    <Route path=path!("/document") view={maybe_lazy!(flams_router_content::components::TopDocRouter)}/>
112                    <Route path=path!("/") view={maybe_lazy!(flams_router_content::components::UriTopRouter)}/>
113                </ParentRoute>
114            </Routes>}
115        }</Router>
116    }.into_any()
117}
118
119maybe_lazy!(Home = home());
120
121fn home() -> AnyView {
122    let index = flams_router_backend::index_components::index();
123    let header = wait_and_then_fn(get_home, |(css, top)| {
124        if let Some(css) = css {
125            inject_css_after("home-main", css);
126        }
127        top.map(|top| {
128            let d = ftml_dom::global_setup(move || {
129                flams_router_content::Views::top(move || {
130                    flams_router_content::Views::setup_document(
131                        DocumentUri::no_doc().clone(),
132                        ftml_components::SidebarPosition::None,
133                        true,
134                        ftml_dom::toc::TocSource::None,
135                        move || flams_router_content::Views::render_ftml(top, None).into_any(),
136                    )
137                })
138            });
139            view! {
140                {d}
141                <hr/>
142            }
143        })
144        .into_any()
145    });
146    view! {
147        {header}
148        {index}
149    }
150    .into_any()
151}
152
153#[cfg(feature = "ssr")]
154static MAIN_DIRS: std::sync::LazyLock<(
155    std::path::PathBuf,
156    std::path::PathBuf,
157    std::path::PathBuf,
158)> = std::sync::LazyLock::new(|| {
159    let ce = std::env::current_exe().expect("failed to determine current executable path");
160    let cedir = ce
161        .parent()
162        .expect("failed to determine current executable path");
163    let main_css = cedir.join("templates/main.css");
164    let main_html = cedir.join("templates/main.html");
165    let logo = cedir.join("templates/logo.html");
166    (main_css, main_html, logo)
167});
168
169#[server]
170pub async fn get_logo() -> Result<Option<String>, ServerFnError<String>> {
171    let (_, _, logo) = &*MAIN_DIRS;
172    Ok(
173        if logo.exists()
174            && let Ok(s) = std::fs::read_to_string(logo)
175        {
176            Some(s)
177        } else {
178            None
179        },
180    )
181}
182
183#[server]
184pub async fn get_home() -> Result<(Option<String>, Option<String>), ServerFnError<String>> {
185    let (css, main, _) = &*MAIN_DIRS;
186    let css = if css.exists() {
187        Some(
188            tokio::fs::read_to_string(css)
189                .await
190                .map_err(|e| ServerFnError::from(e.to_string()))?,
191        )
192    } else {
193        None
194    };
195    let html = if main.exists() {
196        Some(
197            tokio::fs::read_to_string(main)
198                .await
199                .map_err(|e| ServerFnError::from(e.to_string()))?,
200        )
201    } else {
202        None
203    };
204    Ok((css, html))
205}
206
207#[component(transparent)]
208fn Top() -> AnyView {
209    use flams_router_login::components::LoginProvider;
210    provide_context(AllowSubterms(true));
211    view!(<LoginProvider><leptos_router::components::Outlet/></LoginProvider>).into_any()
212}
213
214#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
215enum Page {
216    Home,
217    MathHub,
218    //Graphs,
219    Log,
220    NotFound,
221    Queue,
222    Settings,
223    Login,
224    Query,
225    Search,
226    //FloDown,
227    MyArchives,
228    Users,
229}
230impl Page {
231    pub const fn key(self) -> &'static str {
232        match self {
233            Self::Home => "home",
234            Self::MathHub => "mathhub",
235            //Graphs => "graphs",
236            Self::Log => "log",
237            Self::Login => "login",
238            Self::Queue => "queue",
239            Self::Settings => "settings",
240            Self::Query => "query",
241            Self::MyArchives => "archives",
242            Self::Search => "search",
243            //Self::FloDown => "flodown",
244            Self::Users => "users",
245            Self::NotFound => "notfound",
246        }
247    }
248}
249impl std::fmt::Display for Page {
250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251        f.write_str(self.key())
252    }
253}
254
255#[component(transparent)]
256pub fn Dashboard() -> impl IntoView {
257    view! {
258      <Outlet/>
259    }
260}
261
262fn main_page(page: Page) -> AnyView {
263    fn logo_default() -> impl IntoView {
264        view! {
265            <svg xmlns="http://www.w3.org/2000/svg" width="120px" height="60px" viewBox="0 -805.5 2248.7 1111" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" style="color:var(--colorBrandForeground1)">
266                <defs>
267                    <path id="MJX-5-TEX-SS-1D5A5" d="M86 0V691H526V611H358L190 612V384H485V308H190V0H86Z"></path>
268                    <path id="MJX-5-TEX-SS-1D5AB" d="M87 0V694H191V79L297 80H451L499 81V0H87Z"></path>
269                    <path id="MJX-5-TEX-N-2200" d="M0 673Q0 684 7 689T20 694Q32 694 38 680T82 567L126 451H430L473 566Q483 593 494 622T512 668T519 685Q524 694 538 694Q556 692 556 674Q556 670 426 329T293 -15Q288 -22 278 -22T263 -15Q260 -11 131 328T0 673ZM414 410Q414 411 278 411T142 410L278 55L414 410Z"></path>
270                    <path id="MJX-5-TEX-SS-1D5AC" d="M92 0V694H228L233 680Q236 675 284 547T382 275T436 106Q446 149 497 292T594 558L640 680L645 694H782V0H689V305L688 606Q688 577 500 78L479 23H392L364 96Q364 97 342 156T296 280T246 418T203 544T186 609V588Q185 568 185 517T185 427T185 305V0H92Z"></path>
271                    <path id="MJX-5-TEX-SO-222B" d="M113 -244Q113 -246 119 -251T139 -263T167 -269Q186 -269 199 -260Q220 -247 232 -218T251 -133T262 -15T276 155T297 367Q300 390 305 438T314 512T325 580T340 647T361 703T390 751T428 784T479 804Q481 804 488 804T501 805Q552 802 581 769T610 695Q610 669 594 657T561 645Q542 645 527 658T512 694Q512 705 516 714T526 729T538 737T548 742L552 743Q552 745 545 751T525 762T498 768Q475 768 460 756T434 716T418 652T407 559T398 444T387 300T369 133Q349 -38 337 -102T303 -207Q256 -306 169 -306Q119 -306 87 -272T55 -196Q55 -170 71 -158T104 -146Q123 -146 138 -159T153 -195Q153 -206 149 -215T139 -230T127 -238T117 -242L113 -244Z"></path>
272                </defs>
273                <g stroke="currentcolor" fill="currentcolor" stroke-width="0" transform="scale(1,-1)">
274                    <g data-mml-node="math">
275                        <g data-mml-node="mstyle">
276                            <g data-mml-node="TeXAtom" data-mjx-texclass="ORD">
277                                <g data-mml-node="mi">
278                                    <use data-c="1D5A5" xlink:href="#MJX-5-TEX-SS-1D5A5"></use>
279                                </g>
280                            </g>
281                            <g data-mml-node="mspace" transform="translate(569,0)"></g>
282                            <g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(469,0)">
283                                <g data-mml-node="mi">
284                                    <use data-c="1D5AB" xlink:href="#MJX-5-TEX-SS-1D5AB"></use>
285                                </g>
286                            </g>
287                            <g data-mml-node="mspace" transform="translate(1011,0)"></g>
288                            <g data-mml-node="mpadded" transform="translate(651,0)">
289                                <g transform="translate(0,23)">
290                                    <g data-mml-node="mi">
291                                        <use data-c="2200" xlink:href="#MJX-5-TEX-N-2200"></use>
292                                    </g>
293                                </g>
294                            </g>
295                            <g data-mml-node="mspace" transform="translate(1207,0)"></g>
296                            <g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(1097,0)">
297                                <g data-mml-node="mi">
298                                    <use data-c="1D5AC" xlink:href="#MJX-5-TEX-SS-1D5AC"></use>
299                                </g>
300                            </g>
301                        <g data-mml-node="mspace" transform="translate(1972,0)"></g>
302                        <g data-mml-node="mo" transform="translate(1638.7,0) translate(0 0.5)">
303                            <use data-c="222B" xlink:href="#MJX-5-TEX-SO-222B"></use>
304                        </g>
305                    </g>
306                </g>
307            </g>
308        </svg>}
309    }
310
311    let logo = wait_and_then_fn(get_logo, |logo| {
312        logo.map_or_else(
313            || leptos::either::Either::Left(logo_default()),
314            |s| leptos::either::Either::Right(flams_router_content::Views::render_ftml(s, None)),
315        )
316        .into_any()
317    });
318
319    ftml_dom::global_setup(move || {
320        flams_router_content::Views::top(move || {
321            view! {
322              <div style="width:100vw;height:100vh;"><Layout>
323                  <LayoutHeader style="text-align:center;" slot>
324                    <div style="width:100%">
325                      <Grid cols=3>
326                        <GridItem>""</GridItem>
327                        <GridItem>{logo}</GridItem>
328                        <GridItem>
329                          <div style="width:calc(100% - 20px);text-align:right;padding:10px">
330                            {user_field().into_any()}
331                          </div>
332                        </GridItem>
333                      </Grid>
334                      <Divider/>
335                    </div>
336                  </LayoutHeader>
337                    <LayoutSider class="flams-menu" slot>
338                        {side_menu(page)}
339                    </LayoutSider>
340                    <Outlet/>
341              </Layout></div>
342            }
343        })
344    })
345    .into_any()
346}
347
348#[component]
349fn NotFound() -> AnyView {
350    #[cfg(feature = "ssr")]
351    {
352        let resp = expect_context::<leptos_axum::ResponseOptions>();
353        resp.set_status(axum::http::StatusCode::NOT_FOUND);
354    }
355
356    view! {
357        <h3>"Not Found"</h3>
358    }
359    .into_any()
360}
361
362fn side_menu(page: Page) -> AnyView {
363    use ftml_component_utils::{NavDrawer, NavItem};
364    view! {
365        <NavDrawer selected_value=page.to_string() class="flams-menu-inner">
366            <NavItem value="home" href="/">"Home"</NavItem>
367            <NavItem value="mathhub" href="/dashboard/mathhub">"MathHub"</NavItem>
368            <NavItem value="query" href="/dashboard/query">"Queries"</NavItem>
369            <NavItem value="search" href="/dashboard/search">"Search Content"</NavItem>
370            {move || {let s = LoginState::get(); match s {
371                LoginState::NoAccounts => view!{
372                    <NavItem value="log" href="/dashboard/log">"Logs"</NavItem>
373                    <NavItem value="settings" href="/dashboard/settings">"Settings"</NavItem>
374                    <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
375                    //<NavItem value="flodown" href="/dashboard/flodown">"FloDown"</NavItem>
376                }.into_any(),
377                LoginState::Admin  => view!{
378                  <NavItem value="log" href="/dashboard/log">"Logs"</NavItem>
379                  <NavItem value="settings" href="/dashboard/settings">"Settings"</NavItem>
380                  <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
381                  //<NavItem value="flodown" href="/dashboard/flodown">"FloDown"</NavItem>
382                  <NavItem value="users" href="/dashboard/users">"Manage Users"</NavItem>
383                }.into_any(),
384                LoginState::User{is_admin:true,..} => view!{
385                  <NavItem value="log" href="/dashboard/log">"Logs"</NavItem>
386                  <NavItem value="settings" href="/dashboard/settings">"Settings"</NavItem>
387                  <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
388                  <NavItem value="archives" href="/dashboard/archives">"My Archives"</NavItem>
389                  //<NavItem value="flodown" href="/dashboard/flodown">"FloDown"</NavItem>
390                }.into_any(),
391                LoginState::User{..} => view!{
392                    <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
393                    <NavItem value="archives" href="/dashboard/archives">"My Archives"</NavItem>
394                    //<NavItem value="flodown" href="/dashboard/flodown">"FloDown"</NavItem>
395                }.into_any(),
396                LoginState::None | LoginState::Loading => ().into_any()
397            }}}
398        </NavDrawer>
399    }
400    .into_any()
401}
402
403fn user_field() -> AnyView {
404    use ftml_component_utils::{
405        Menu, MenuItem, MenuPosition, MenuTrigger, MenuTriggerType, Spinner, theming::Theme,
406    };
407
408    view! {//<ClientOnly>
409        <div class="flams-user-menu-trigger">{
410        let theme = expect_context::<RwSignal<Theme>>();
411        let on_select = move |key: &'static str| match key {
412            "theme" => {
413                theme.update(|v| {
414                    if v.name == "dark" {
415                        *v = Theme::light();
416                    } else {
417                        *v = Theme::dark();
418                    }
419                });
420            }
421            _ => unreachable!(),
422        };
423        let src = Memo::new(|_| match LoginState::get() {
424            LoginState::User { avatar, .. } => Some(avatar),
425            LoginState::Admin => Some("/admin.png".to_string()),
426            _ => None,
427        });
428        let icon = Memo::new(move |_| if theme.with(|v| v.name == "dark")
429            {icondata_bi::BiSunRegular} else {icondata_bi::BiMoonSolid}
430        );
431        let text = Memo::new(move |_| if theme.with(|v| v.name == "dark")
432            {"Light Mode"} else {"Dark Mode"}
433        );
434        view!{
435        <Menu on_select trigger_type=MenuTriggerType::Hover position=MenuPosition::Bottom>
436            <MenuTrigger slot>
437                <ftml_component_utils::Avatar src />
438            </MenuTrigger>
439            // AiGitlabFilled
440            <MenuItem value="theme" icon=icon>{text}</MenuItem>
441            <Divider/>
442            {move || match LoginState::get() {
443                LoginState::None => login_form().into_any(),
444                LoginState::NoAccounts => view!(<span>"Admin"</span>).into_any(),
445                LoginState::Admin => logout_form("admin".to_string()).into_any(),
446                LoginState::User{name,..} => logout_form(name).into_any(),
447                LoginState::Loading => view!(<Spinner small=true/>).into_any()
448            }}
449        </Menu>
450        }
451    }</div>
452    //</ClientOnly>
453    }
454    .into_any()
455}
456
457fn logout_form(user: String) -> AnyView {
458    use ftml_component_utils::Button;
459    let login = expect_context::<RwSignal<LoginState>>();
460    let action = Action::new(move |_| {
461        login.set(LoginState::None);
462        flams_router_login::server_fns::logout()
463    });
464    view!(<span>{user}" "<Button on_click=move |_| {action.dispatch(());}>Logout</Button></span>)
465        .into_any()
466}
467
468fn login_form() -> AnyView {
469    use ftml_component_utils::{Button, Input, InputType};
470    let login = expect_context();
471    let action = Action::new(move |pwd: &String| do_login(pwd.clone(), login));
472    let value = RwSignal::<String>::new(String::new());
473    view! {
474      <Button on_click=move |_| {action.dispatch(value.get_untracked());}>Login</Button>
475      <Input placeholder="admin pwd" value input_type=InputType::Password/>
476    }
477    .into_any()
478}
479
480#[allow(unused_variables)]
481async fn do_login(pw: String, login: RwSignal<LoginState>) {
482    let pwd = if pw.is_empty() { None } else { Some(pw) };
483    match flams_router_login::server_fns::login(pwd).await {
484        Ok(Some(u @ (LoginState::Admin | LoginState::User { .. }))) => login.set(u),
485        Ok(_) => (),
486        Err(e) => {
487            #[cfg(feature = "hydrate")]
488            flams_web_utils::components::display_error(std::borrow::Cow::Owned(format!(
489                "Error: {e}"
490            )));
491        }
492    }
493    let _ = view!(<Redirect path="/dashboard"/>);
494}