Skip to content

Commit

Permalink
Improve EOSdash.
Browse files Browse the repository at this point in the history
Make EOSdash use UI components from MonsterUI to ease further development.

- Add a first menu with some dummy pages and the configuration page.
- Make the configuration scrollable.
- Add a footer that displays connection status with EOS server

Update EOS server:

- Provide health endpoint for alive checking.
- Move error message generation to extra module

Signed-off-by: Bobby Noelte <[email protected]>
  • Loading branch information
b0661 committed Jan 26, 2025
1 parent 26762e5 commit e12109b
Show file tree
Hide file tree
Showing 12 changed files with 977 additions and 454 deletions.
20 changes: 18 additions & 2 deletions docs/_generated/openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,9 @@ Returns:

---

## PUT /v1/config/reset
## POST /v1/config/reset

**Links**: [local](http://localhost:8503/docs#/default/fastapi_config_update_post_v1_config_reset_put), [eos](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_config_update_post_v1_config_reset_put)
**Links**: [local](http://localhost:8503/docs#/default/fastapi_config_update_post_v1_config_reset_post), [eos](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_config_update_post_v1_config_reset_post)

Fastapi Config Update Post

Expand All @@ -257,6 +257,22 @@ Returns:

---

## GET /v1/health

**Links**: [local](http://localhost:8503/docs#/default/fastapi_health_get_v1_health_get), [eos](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_health_get_v1_health_get)

Fastapi Health Get

```
Health check endpoint to verify that the server is alive.
```

**Responses**:

- **200**: Successful Response

---

## PUT /v1/measurement/data

**Links**: [local](http://localhost:8503/docs#/default/fastapi_measurement_data_put_v1_measurement_data_put), [eos](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_measurement_data_put_v1_measurement_data_put)
Expand Down
501 changes: 259 additions & 242 deletions openapi.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ numpydantic==1.6.4
matplotlib==3.10.0
fastapi[standard]==0.115.6
python-fasthtml==0.12.0
MonsterUI==0.0.29
mistletoe==1.4.0
lxml==5.3.0
uvicorn==0.34.0
scikit-learn==1.6.1
timezonefinder==6.5.7
Expand Down
Empty file.
179 changes: 179 additions & 0 deletions src/akkudoktoreos/server/dash/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
from typing import Any, Optional, Union

from fasthtml.common import FT, H1, Div, Li
from monsterui.foundations import stringify
from monsterui.franken import (
Button,
ButtonT,
Card,
Container,
ContainerT,
TabContainer,
render_md,
)

scrollbar_viewport_styles = (
"scrollbar-width: none; -ms-overflow-style: none; -webkit-overflow-scrolling: touch;"
)

scrollbar_cls = "flex touch-none select-none transition-colors p-[1px]"


def ScrollArea(
*c: Any, cls: Optional[Union[str, tuple]] = None, orientation: str = "vertical", **kwargs: Any
) -> Div:
"""Creates a styled scroll area.
Args:
orientation (str): The orientation of the scroll area. Defaults to vertical.
"""
new_cls = "relative overflow-hidden"
if cls:
new_cls += f" {stringify(cls)}"
kwargs["cls"] = new_cls

content = Div(
Div(*c, style="min-width:100%;display:table;"),
style=f"overflow: {'hidden scroll' if orientation == 'vertical' else 'scroll'}; {scrollbar_viewport_styles}",
cls="w-full h-full rounded-[inherit]",
data_ref="viewport",
)

scrollbar = Div(
Div(cls="bg-border rounded-full hidden relative flex-1", data_ref="thumb"),
cls=f"{scrollbar_cls} flex-col h-2.5 w-full border-t border-t-transparent"
if orientation == "horizontal"
else f"{scrollbar_cls} w-2.5 h-full border-l border-l-transparent",
data_ref="scrollbar",
style=f"position: absolute;{'right:0; top:0;' if orientation == 'vertical' else 'bottom:0; left:0;'}",
)

return Div(
content,
scrollbar,
role="region",
tabindex="0",
data_orientation=orientation,
data_ref_scrollarea=True,
aria_label="Scrollable content",
**kwargs,
)


def Markdown(md: str) -> FT:
return render_md(md)


def DashboardHeader(title: Optional[str]) -> Div:
"""Creates a styled header with a title.
Args:
title (Optional[str]): The title text for the header.
Returns:
Div: A styled `Div` element containing the header.
"""
if title is None:
return Div("", cls="header")
return Div(H1(title, cls="text-2xl font-bold mb-4"), cls="header")


def DashboardFooter(path: str) -> Card:
"""Creates a styled footer with the provided information.
The footer content is reloaded every 5 seconds from path.
Args:
path (str): Path to reload footer content from
Returns:
Card: A styled `Card` element containing the footer.
"""
return Card(
Container("Footer", id="footer-content"),
hx_get=f"{path}",
hx_trigger="every 5s",
hx_target="#footer-content",
hx_swap="innerHTML",
)


def DashboardTrigger(*c: Any, cls: Optional[Union[str, tuple]] = None, **kwargs: Any) -> Button:
"""Creates a styled button for the dashboard trigger.
Args:
*c: Positional arguments to pass to the button.
cls (Optional[str]): Additional CSS classes for styling. Defaults to None.
**kwargs: Additional keyword arguments for the button.
Returns:
Button: A styled `Button` component.
"""
new_cls = f"{ButtonT.primary}"
if cls:
new_cls += f" {stringify(cls)}"
kwargs["cls"] = new_cls
return Button(*c, submit=False, **kwargs)


def DashboardTabs(dashboard_items: dict[str, str]) -> Card:
"""Creates a dashboard tab with dynamic dashboard items.
Args:
dashboard_items (dict[str, str]): A dictionary of dashboard items where keys are item names
and values are paths for navigation.
Returns:
Card: A styled `Card` component containing the dashboard tabs.
"""
dash_items = [
Li(
DashboardTrigger(
menu,
hx_get=f"{path}",
hx_target="#page-content",
hx_swap="innerHTML",
),
)
for menu, path in dashboard_items.items()
]
return Card(TabContainer(*dash_items, cls="gap-4"), alt=True)


def DashboardContent(content: Any) -> Card:
"""Creates a content section within a styled card.
Args:
content (Any): The content to display.
Returns:
Card: A styled `Card` element containing the content.
"""
return Card(ScrollArea(Container(content, id="page-content"), cls="h-[75vh] w-full rounded-md"))


def Page(
title: Optional[str],
dashboard_items: dict[str, str],
content: Any,
footer_path: str,
) -> Div:
"""Generates a full-page layout with a header, dashboard items, content, and footer.
Args:
title (Optional[str]): The page title.
dashboard_items (dict[str, str]): A dictionary of dashboard items.
content (Any): The main content for the page.
footer_content (Any): Footer content.
footer_path (Any): Path to reload footer content from.
Returns:
Div: A `Div` element representing the entire page layout.
"""
return Container(
DashboardHeader(title),
DashboardTabs(dashboard_items),
DashboardContent(content),
DashboardFooter(footer_path),
cls=("w-screen p-4 space-y-4", ContainerT.xl),
)
Loading

0 comments on commit e12109b

Please sign in to comment.