Skip to main content

flams_router_dashboard/
lib.rs

1#![recursion_limit = "512"]
2#![allow(clippy::must_use_candidate)]
3
4#[cfg(any(
5    all(feature = "ssr", feature = "hydrate", not(feature = "docs-only")),
6    not(any(feature = "ssr", feature = "hydrate")),
7))]
8compile_error!("exactly one of the features \"ssr\" or \"hydrate\" must be enabled");
9
10pub mod query;
11mod settings;
12
13pub mod ws {
14    pub use flams_flodown::math::MathSocket;
15    #[cfg(feature = "ssr")]
16    pub use flams_flodown::math::TeXSocket;
17    pub use flams_router_base::ws::*;
18    pub use flams_router_buildqueue_components::QueueSocket;
19    pub use flams_router_logging::LogSocket;
20}
21
22pub mod server_fns {
23    pub mod content {
24        pub use flams_router_content::server_fns::*;
25    }
26    pub mod backend {
27        pub use flams_router_backend::server_fns::*;
28    }
29    pub mod buildqueue {
30        pub use flams_router_buildqueue_base::server_fns::*;
31    }
32    pub mod git {
33        pub use flams_router_git_base::server_fns::*;
34    }
35    pub mod login {
36        pub use flams_router_login::server_fns::*;
37    }
38    pub mod search {
39        pub use flams_router_search::{search_query, search_symbols};
40    }
41    pub use super::query::query_api as query;
42    pub use super::settings::{get_settings as settings, reload};
43}
44
45pub use flams_router_base::LoginState;
46use flams_router_base::maybe_lazy;
47use flams_web_utils::components::{Layout, LayoutHeader, LayoutSider};
48use ftml_component_utils::{Divider, Grid, GridItem};
49use ftml_components::config::AllowSubterms;
50use ftml_dom::FtmlViews;
51use leptos::prelude::*;
52use leptos_meta::{Title, provide_meta_context};
53use leptos_router::{
54    components::{Outlet, ParentRoute, Redirect, Route, Router, Routes},
55    path,
56};
57
58#[component]
59pub fn Main() -> AnyView {
60    provide_meta_context();
61    let (_, set_is_routing) = signal(false);
62    view! {
63        <Title text="𝖥𝖫∀𝖬∫"/>
64        <Router set_is_routing>{
65            //let has_params = Memo::new(move |_| use_query_map().with(|p| p.get_str("a").is_some() || p.get_str("uri").is_some()));
66            view!{<Routes fallback=|| NotFound()>
67                <ParentRoute/* ssr=SsrMode::InOrder*/ path=() view=Top>
68                    <ParentRoute path=path!("/dashboard") view=Dashboard>
69                        <ParentRoute path=path!("mathhub") view={|| main_page(Page::MathHub)}>
70                            <Route path=path!("") view={maybe_lazy!(flams_router_backend::components::ArchivesTop)}/>
71                        </ParentRoute>
72                        //<Route path="graphs" view=|| view!(<MainPage page=Page::Graphs/>)/>
73                        <ParentRoute path=path!("log") view={|| main_page(Page::Log)}>
74                            <Route path=path!("") view={maybe_lazy!(flams_router_logging::Logger)}/>
75                        </ParentRoute>
76                        <ParentRoute path=path!("queue") view={|| main_page(Page::Queue)}>
77                            <Route path=path!("") view={maybe_lazy!(flams_router_buildqueue_components::QueuesTop)}/>
78                        </ParentRoute>
79                        <ParentRoute path=path!("settings") view={|| main_page(Page::Settings)}>
80                            <Route path=path!("") view={maybe_lazy!(settings::Settings)}/>
81                        </ParentRoute>
82                        <ParentRoute path=path!("query") view={|| main_page(Page::Query)}>
83                            <Route path=path!("") view={maybe_lazy!(query::Query)}/>
84                        </ParentRoute>
85                        <ParentRoute path=path!("archives") view={|| main_page(Page::MyArchives)}>
86                            <Route path=path!("") view={maybe_lazy!(flams_router_git_components::Archives)}/>
87                        </ParentRoute>
88                        <ParentRoute path=path!("users") view={|| main_page(Page::Users)}>
89                            <Route path=path!("") view={maybe_lazy!(flams_router_login::components::Users)}/>
90                        </ParentRoute>
91                        <ParentRoute path=path!("search") view={|| main_page(Page::Search)}>
92                            <Route path=path!("") view={maybe_lazy!(flams_router_search::components::SearchTop)}/>
93                        </ParentRoute>
94                        <ParentRoute path=path!("flodown") view={|| main_page(Page::FloDown)}>
95                            <Route path=path!("") view={maybe_lazy!(flams_flodown::FloDownEditor)}/>
96                        </ParentRoute>
97                        <ParentRoute path=path!("") view={|| main_page(Page::Home)}>
98                            <Route path=path!("") view={maybe_lazy!(flams_router_backend::index_components::Index)}/>
99                        </ParentRoute>
100                        <ParentRoute path=path!("*any") view={|| main_page(Page::NotFound)}>
101                            <Route path=path!("") view=||view!(<NotFound/>)/>
102                        </ParentRoute>
103                    </ParentRoute>
104                    <ParentRoute path=path!("/vscode") view={maybe_lazy!(flams_router_vscode::VSCWrap)}>// flams_router_vscode::VSCodeWrap>
105                        <Route path=path!("search") view={maybe_lazy!(flams_router_search::vscode::VSCSearch)}/>
106                        <Route path=path!("proofs") view={maybe_lazy!(flams_router_vscode::checks::Checks)}/>
107                    </ParentRoute>
108                    <Route path=path!("/document") view={maybe_lazy!(flams_router_content::components::TopDocRouter)}/>
109                    <Route path=path!("/") view={maybe_lazy!(flams_router_content::components::UriTopRouter)}/>
110                </ParentRoute>
111            </Routes>}
112        }</Router>
113    }.into_any()
114}
115
116#[component(transparent)]
117fn Top() -> AnyView {
118    use flams_router_login::components::LoginProvider;
119    provide_context(AllowSubterms(true));
120    view!(<LoginProvider><leptos_router::components::Outlet/></LoginProvider>).into_any()
121}
122
123#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
124enum Page {
125    Home,
126    MathHub,
127    //Graphs,
128    Log,
129    NotFound,
130    Queue,
131    Settings,
132    Login,
133    Query,
134    Search,
135    FloDown,
136    MyArchives,
137    Users,
138}
139impl Page {
140    pub const fn key(self) -> &'static str {
141        match self {
142            Self::Home => "home",
143            Self::MathHub => "mathhub",
144            //Graphs => "graphs",
145            Self::Log => "log",
146            Self::Login => "login",
147            Self::Queue => "queue",
148            Self::Settings => "settings",
149            Self::Query => "query",
150            Self::MyArchives => "archives",
151            Self::Search => "search",
152            Self::FloDown => "flodown",
153            Self::Users => "users",
154            Self::NotFound => "notfound",
155        }
156    }
157}
158impl std::fmt::Display for Page {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        f.write_str(self.key())
161    }
162}
163
164#[component(transparent)]
165pub fn Dashboard() -> impl IntoView {
166    view! {
167      <Outlet/>
168    }
169}
170
171fn main_page(page: Page) -> AnyView {
172    //use flams_web_utils::components::Themer;
173    /*view! {
174    <Themer>{*/
175    //flams_router_content::Views::top(move || {
176    ftml_dom::global_setup(move || flams_router_content::Views::top(move || {
177    view! {
178          <div style="width:100vw;height:100vh;"><Layout>
179            //<Login>
180              <LayoutHeader style="text-align:center;" slot>
181                <div style="width:100%">
182                  <Grid cols=3>
183                    <GridItem>""</GridItem>
184                    <GridItem>
185                    <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)"><defs><path id="MJX-5-TEX-SS-1D5A5" d="M86 0V691H526V611H358L190 612V384H485V308H190V0H86Z"></path><path id="MJX-5-TEX-SS-1D5AB" d="M87 0V694H191V79L297 80H451L499 81V0H87Z"></path><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><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><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></defs><g stroke="currentcolor" fill="currentcolor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mstyle"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><use data-c="1D5A5" xlink:href="#MJX-5-TEX-SS-1D5A5"></use></g></g><g data-mml-node="mspace" transform="translate(569,0)"></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(469,0)"><g data-mml-node="mi"><use data-c="1D5AB" xlink:href="#MJX-5-TEX-SS-1D5AB"></use></g></g><g data-mml-node="mspace" transform="translate(1011,0)"></g><g data-mml-node="mpadded" transform="translate(651,0)"><g transform="translate(0,23)"><g data-mml-node="mi"><use data-c="2200" xlink:href="#MJX-5-TEX-N-2200"></use></g></g></g><g data-mml-node="mspace" transform="translate(1207,0)"></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(1097,0)"><g data-mml-node="mi"><use data-c="1D5AC" xlink:href="#MJX-5-TEX-SS-1D5AC"></use></g></g><g data-mml-node="mspace" transform="translate(1972,0)"></g><g data-mml-node="mo" transform="translate(1638.7,0) translate(0 0.5)"><use data-c="222B" xlink:href="#MJX-5-TEX-SO-222B"></use></g></g></g></g></svg>
186                      /*<h1 style="font-family:serif;color:var(--colorBrandForeground1)">
187                        "𝖥𝖫∀𝖬∫"
188                      </h1>*/
189                    </GridItem>
190                    <GridItem>
191                      <div style="width:calc(100% - 20px);text-align:right;padding:10px">
192                        {user_field().into_any()}
193                      </div>
194                    </GridItem>
195                  </Grid>
196                  <Divider/>
197                </div>
198              </LayoutHeader>
199              //<Layout>
200                  <LayoutSider class="flams-menu" slot>
201                    {side_menu(page)}
202                  </LayoutSider>
203                  //<Layout>
204                    //<div style="width:calc(100% - 10px);padding-left:5px;height:calc(100vh - 67px)">
205                      <Outlet/>//{do_main(page).into_any()}
206                    //</div>
207                  //</Layout>
208                //</Layout>
209            //</Login>
210          </Layout></div>
211        }
212    })).into_any()
213}
214
215/*
216fn do_main(page: Page) -> AnyView {
217    //leptos::logging::log!("Here!");
218    let inner = || match page {
219        Page::Home => view!(<flams_router_backend::index_components::Index/>).into_any(),
220        Page::MathHub => view! {<flams_router_backend::components::ArchivesTop/>}.into_any(),
221        //Page::Graphs => view!{<GraphTest/>},
222        Page::Log => view! {<flams_router_logging::Logger/>}.into_any(),
223        Page::Queue => view! {<flams_router_buildqueue_components::QueuesTop/>}.into_any(),
224        Page::Query => view! {<query::Query/>}.into_any(),
225        Page::Settings => view! {<settings::Settings/>}.into_any(),
226        Page::MyArchives => view! {<flams_router_git_components::Archives/>}.into_any(),
227        Page::Search => flams_router_search::components::search_top().into_any(),
228        Page::FloDown => view! {<flams_flodown::FloDownEditor/>}.into_any(),
229        Page::Users => view! {<flams_router_login::components::Users/>}.into_any(),
230        _ => view!(<span>"TODO"</span>).into_any(),
231        //Page::Login => view!{<LoginPage/>}
232    };
233    view!(<main style="height:100%">{inner()}</main>).into_any()
234}
235 */
236
237#[component]
238fn NotFound() -> AnyView {
239    #[cfg(feature = "ssr")]
240    {
241        let resp = expect_context::<leptos_axum::ResponseOptions>();
242        resp.set_status(axum::http::StatusCode::NOT_FOUND);
243    }
244
245    view! {
246        <h3>"Not Found"</h3>
247    }
248    .into_any()
249}
250
251fn side_menu(page: Page) -> AnyView {
252    use ftml_component_utils::{NavDrawer, NavItem};
253    view! {
254        <NavDrawer selected_value=page.to_string() class="flams-menu-inner">
255            <NavItem value="home" href="/">"Home"</NavItem>
256            <NavItem value="mathhub" href="/dashboard/mathhub">"MathHub"</NavItem>
257            <NavItem value="query" href="/dashboard/query">"Queries"</NavItem>
258            <NavItem value="search" href="/dashboard/search">"Search Content"</NavItem>
259            {move || {let s = LoginState::get(); match s {
260                LoginState::NoAccounts => view!{
261                    <NavItem value="log" href="/dashboard/log">"Logs"</NavItem>
262                    <NavItem value="settings" href="/dashboard/settings">"Settings"</NavItem>
263                    <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
264                    <NavItem value="flodown" href="/dashboard/flodown">"FloDown"</NavItem>
265                }.into_any(),
266                LoginState::Admin  => view!{
267                  <NavItem value="log" href="/dashboard/log">"Logs"</NavItem>
268                  <NavItem value="settings" href="/dashboard/settings">"Settings"</NavItem>
269                  <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
270                  <NavItem value="flodown" href="/dashboard/flodown">"FloDown"</NavItem>
271                  <NavItem value="users" href="/dashboard/users">"Manage Users"</NavItem>
272                }.into_any(),
273                LoginState::User{is_admin:true,..} => view!{
274                  <NavItem value="log" href="/dashboard/log">"Logs"</NavItem>
275                  <NavItem value="settings" href="/dashboard/settings">"Settings"</NavItem>
276                  <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
277                  <NavItem value="archives" href="/dashboard/archives">"My Archives"</NavItem>
278                  <NavItem value="flodown" href="/dashboard/flodown">"FloDown"</NavItem>
279                }.into_any(),
280                LoginState::User{..} => view!{
281                    <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
282                    <NavItem value="archives" href="/dashboard/archives">"My Archives"</NavItem>
283                    <NavItem value="flodown" href="/dashboard/flodown">"FloDown"</NavItem>
284                }.into_any(),
285                LoginState::None | LoginState::Loading => ().into_any()
286            }}}
287        </NavDrawer>
288    }
289    .into_any()
290}
291
292fn user_field() -> AnyView {
293    use ftml_component_utils::{
294        Menu, MenuItem, MenuPosition, MenuTrigger, MenuTriggerType, Spinner, theming::Theme,
295    };
296
297    view! {//<ClientOnly>
298        <div class="flams-user-menu-trigger">{
299        let theme = expect_context::<RwSignal<Theme>>();
300        let on_select = move |key: &'static str| match key {
301            "theme" => {
302                theme.update(|v| {
303                    if v.name == "dark" {
304                        *v = Theme::light();
305                    } else {
306                        *v = Theme::dark();
307                    }
308                });
309            }
310            _ => unreachable!(),
311        };
312        let src = Memo::new(|_| match LoginState::get() {
313            LoginState::User { avatar, .. } => Some(avatar),
314            LoginState::Admin => Some("/admin.png".to_string()),
315            _ => None,
316        });
317        let icon = Memo::new(move |_| if theme.with(|v| v.name == "dark")
318            {icondata_bi::BiSunRegular} else {icondata_bi::BiMoonSolid}
319        );
320        let text = Memo::new(move |_| if theme.with(|v| v.name == "dark")
321            {"Light Mode"} else {"Dark Mode"}
322        );
323        view!{
324        <Menu on_select trigger_type=MenuTriggerType::Hover position=MenuPosition::Bottom>
325            <MenuTrigger slot>
326                <ftml_component_utils::Avatar src />
327            </MenuTrigger>
328            // AiGitlabFilled
329            <MenuItem value="theme" icon=icon>{text}</MenuItem>
330            <Divider/>
331            {move || match LoginState::get() {
332                LoginState::None => login_form().into_any(),
333                LoginState::NoAccounts => view!(<span>"Admin"</span>).into_any(),
334                LoginState::Admin => logout_form("admin".to_string()).into_any(),
335                LoginState::User{name,..} => logout_form(name).into_any(),
336                LoginState::Loading => view!(<Spinner small=true/>).into_any()
337            }}
338        </Menu>
339        }
340    }</div>
341    //</ClientOnly>
342    }
343    .into_any()
344}
345
346fn logout_form(user: String) -> AnyView {
347    use ftml_component_utils::Button;
348    let login = expect_context::<RwSignal<LoginState>>();
349    let action = Action::new(move |_| {
350        login.set(LoginState::None);
351        flams_router_login::server_fns::logout()
352    });
353    view!(<span>{user}" "<Button on_click=move |_| {action.dispatch(());}>Logout</Button></span>)
354        .into_any()
355}
356
357fn login_form() -> AnyView {
358    use ftml_component_utils::{Button, Input, InputType};
359    let login = expect_context();
360    let action = Action::new(move |pwd: &String| do_login(pwd.clone(), login));
361    let value = RwSignal::<String>::new(String::new());
362    view! {
363      <Button on_click=move |_| {action.dispatch(value.get_untracked());}>Login</Button>
364      <Input placeholder="admin pwd" value input_type=InputType::Password/>
365    }
366    .into_any()
367}
368
369#[allow(unused_variables)]
370async fn do_login(pw: String, login: RwSignal<LoginState>) {
371    let pwd = if pw.is_empty() { None } else { Some(pw) };
372    match flams_router_login::server_fns::login(pwd).await {
373        Ok(Some(u @ (LoginState::Admin | LoginState::User { .. }))) => login.set(u),
374        Ok(_) => (),
375        Err(e) => {
376            #[cfg(feature = "hydrate")]
377            flams_web_utils::components::display_error(std::borrow::Cow::Owned(format!(
378                "Error: {e}"
379            )));
380        }
381    }
382    let _ = view!(<Redirect path="/dashboard"/>);
383}