Skip to content

Commit 8fb09d1

Browse files
author
Andrei Neagu
committed
refactored UI first version
1 parent 5b0a357 commit 8fb09d1

File tree

5 files changed

+203
-26
lines changed

5 files changed

+203
-26
lines changed

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_common.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,14 @@
55

66

77
@contextmanager
8-
def base_page(*, title: str | None = None, colour: str = "#D3D3D3") -> Iterator[None]:
8+
def base_page(*, title: str | None = None) -> Iterator[None]:
99
display_title = (
1010
"Dynamic Scheduler" if title is None else f"Dynamic Scheduler - {title}"
1111
)
1212
ui.page_title(display_title)
13-
with ui.header(elevated=True).style(f"background-color: {colour}").classes(
14-
"items-center justify-between"
15-
):
16-
ui.label("HEADER")
1713

18-
yield None
14+
with ui.header(elevated=True).classes("items-center"):
15+
ui.button(icon="o_home", on_click=lambda: ui.navigate.to("/"))
16+
ui.label(display_title)
1917

20-
with ui.footer().style(f"background-color: {colour}"):
21-
ui.label("FOOTER")
18+
yield None
Lines changed: 113 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,145 @@
1+
import json
2+
3+
import arrow
4+
import httpx
15
from fastapi import FastAPI
26
from models_library.projects_nodes_io import NodeID
37
from nicegui import APIRouter, app, ui
48
from nicegui.element import Element
9+
from settings_library.utils_service import DEFAULT_FASTAPI_PORT
510

611
from ...services.service_tracker import TrackedServiceModel, get_all_tracked_services
12+
from ...services.service_tracker._models import SchedulerServiceState
713
from ._common import base_page
814
from ._utils import get_parent_app
915

1016
router = APIRouter()
1117

1218

19+
def _get_elapsed(timestamp: float) -> str:
20+
elapsed_time = arrow.utcnow() - arrow.get(timestamp)
21+
22+
days = elapsed_time.days
23+
hours, remainder = divmod(elapsed_time.seconds, 3600)
24+
minutes, seconds = divmod(remainder, 60)
25+
26+
# Format as "days hours:minutes:seconds"
27+
return f"{days} days, {hours:02}:{minutes:02}:{seconds:02} ago"
28+
29+
30+
def _render_service_details(node_id: NodeID, service: TrackedServiceModel) -> None:
31+
dict_to_render: dict[str, tuple[str, str]] = {
32+
"NodeID": ("code", f"{node_id}"),
33+
"Display State": ("label", service.current_state),
34+
"Last State Change": ("label", _get_elapsed(service.last_state_change)),
35+
"UserID": ("code", f"{service.user_id}"),
36+
"ProjectID": ("code", f"{service.project_id}"),
37+
"User Requested": ("label", service.requested_state),
38+
}
39+
40+
if service.dynamic_service_start:
41+
dict_to_render["Service"] = (
42+
"label",
43+
f"{service.dynamic_service_start.key}:{service.dynamic_service_start.version}",
44+
)
45+
dict_to_render["Product"] = (
46+
"label",
47+
service.dynamic_service_start.product_name,
48+
)
49+
service_status = json.loads(service.service_status) or {}
50+
dict_to_render["Service State"] = (
51+
"label",
52+
service_status.get("service_state", ""),
53+
)
54+
55+
with ui.column().classes("p-0 m-0"):
56+
for key, (widget, value) in dict_to_render.items():
57+
with ui.row(align_items="baseline").classes("p-0 m-0"):
58+
ui.label(key).classes("font-bold")
59+
match widget:
60+
case "code":
61+
ui.code(value)
62+
case "label":
63+
ui.label(value)
64+
case _:
65+
ui.label(value)
66+
67+
68+
def _render_buttons(node_id: NodeID, service: TrackedServiceModel) -> None:
69+
with ui.row(align_items="baseline").classes("p-0 m-0"):
70+
ui.button(
71+
"Details",
72+
icon="source",
73+
on_click=lambda: ui.navigate.to(f"/service/{node_id}:details"),
74+
)
75+
76+
def _render_progress() -> None:
77+
ui.spinner(size="lg")
78+
79+
storage_key = f"removing-{node_id}"
80+
if app.storage.general.get(storage_key, None):
81+
# removal is in progress just render progress bar
82+
_render_progress()
83+
return
84+
85+
if service.current_state == SchedulerServiceState.RUNNING:
86+
with ui.row(align_items="baseline").classes("p-0 m-0") as container:
87+
88+
async def async_task():
89+
container.clear()
90+
91+
_render_progress()
92+
app.storage.general[storage_key] = True
93+
94+
ui.notify(f"Started service stop request for {node_id}")
95+
96+
await httpx.AsyncClient(timeout=10).get(
97+
f"http://localhost:{DEFAULT_FASTAPI_PORT}/service/{node_id}:stop"
98+
)
99+
100+
app.storage.general.pop("removing-{node_id}", None)
101+
ui.notify(f"Finished service stop request for {node_id}")
102+
103+
ui.button("Stop service", icon="stop", on_click=async_task)
104+
105+
13106
def _render_card(
14107
card_container: Element, node_id: NodeID, service: TrackedServiceModel
15108
) -> None:
16109
with card_container: # noqa: SIM117
17-
with ui.card():
18-
ui.label(f"{node_id}")
19-
# TODO finish card
20-
ui.label(service.model_dump_json())
110+
with ui.column().classes("border p-0 m-0"):
111+
_render_service_details(node_id, service)
112+
_render_buttons(node_id, service)
21113

22114

23-
async def _update_cards(parent_app: FastAPI, card_container) -> None:
24-
card_container.clear() # Clear the current cards
115+
class CardUpdater:
116+
def __init__(self, parent_app: FastAPI, container: Element) -> None:
117+
self.parent_app = parent_app
118+
self.container = container
25119

26-
tracked_services = await get_all_tracked_services(parent_app)
120+
async def update(self) -> None:
121+
# TODO: rerender only if data changed
27122

28-
for node_id, service in tracked_services.items():
29-
_render_card(card_container, node_id, service)
123+
self.container.clear() # Clear the current cards
30124

125+
tracked_services = await get_all_tracked_services(self.parent_app)
126+
127+
for node_id, service in tracked_services.items():
128+
_render_card(self.container, node_id, service)
31129

32-
# changed this form ui to router
33-
@router.page("/")
34-
async def main_page():
35-
parent_app = get_parent_app(app)
36130

131+
@router.page("/")
132+
async def index():
37133
with base_page():
38134

39135
# Initial UI setup
40136
ui.label("Dynamic Item List")
41137

42138
card_container: Element = ui.row()
43139

44-
# render cards when page is loaded
45-
await _update_cards(parent_app, card_container)
140+
updater = CardUpdater(get_parent_app(app), card_container)
46141

142+
# render cards when page is loaded
143+
await updater.update()
47144
# update card at a set interval
48-
ui.timer(1.0, lambda: _update_cards(parent_app, card_container))
145+
ui.timer(1, lambda: updater.update())
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from nicegui import APIRouter
2+
3+
from . import _index, _service
4+
5+
router = APIRouter()
6+
7+
router.include_router(_index.router)
8+
router.include_router(_service.router)
9+
10+
__all__: tuple[str, ...] = ("router",)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import json
2+
3+
from models_library.projects_nodes_io import NodeID
4+
from nicegui import APIRouter, app, ui
5+
from servicelib.rabbitmq.rpc_interfaces.dynamic_scheduler.services import (
6+
DynamicServiceStop,
7+
stop_dynamic_service,
8+
)
9+
from simcore_service_dynamic_scheduler.services.rabbitmq import get_rabbitmq_rpc_client
10+
11+
from ...core.settings import ApplicationSettings
12+
from ...services.service_tracker import get_tracked_service
13+
from ._common import base_page
14+
from ._utils import get_parent_app
15+
16+
router = APIRouter()
17+
18+
19+
@router.page("/service/{node_id}:details")
20+
async def service_details(node_id: NodeID):
21+
with base_page(title=f"{node_id} details"):
22+
service_model = await get_tracked_service(get_parent_app(app), node_id)
23+
24+
if not service_model:
25+
ui.markdown(
26+
f"Sorry could not find any details for **node_id={node_id}**. "
27+
"Please make sure the **node_id** is correct. "
28+
"Also make sure you have not provided a **product_id**."
29+
)
30+
return
31+
32+
scheduler_internals = service_model.model_dump(mode="json")
33+
service_status = json.loads(scheduler_internals.pop("service_status"))
34+
dynamic_service_start = scheduler_internals.pop("dynamic_service_start")
35+
36+
ui.markdown("**Service Status**")
37+
ui.code(json.dumps(service_status, indent=2), language="json")
38+
39+
ui.markdown("**Scheduler Internals**")
40+
ui.code(json.dumps(scheduler_internals, indent=2), language="json")
41+
42+
ui.markdown("**Start Parameters**")
43+
ui.code(json.dumps(dynamic_service_start, indent=2), language="json")
44+
45+
46+
@router.page("/service/{node_id}:stop")
47+
async def service_stop(node_id: NodeID):
48+
with base_page(title=f"{node_id} details"):
49+
parent_app = get_parent_app(app)
50+
51+
service_model = await get_tracked_service(parent_app, node_id)
52+
if not service_model:
53+
ui.notify(f"Could not stop service {node_id}. Was not abel to find it")
54+
return
55+
56+
settings: ApplicationSettings = parent_app.state.settings
57+
58+
assert service_model.user_id # nosec
59+
assert service_model.project_id # nosec
60+
61+
await stop_dynamic_service(
62+
get_rabbitmq_rpc_client(get_parent_app(app)),
63+
dynamic_service_stop=DynamicServiceStop(
64+
user_id=service_model.user_id,
65+
project_id=service_model.project_id,
66+
node_id=node_id,
67+
simcore_user_agent="",
68+
save_state=True,
69+
),
70+
timeout_s=int(
71+
settings.DYNAMIC_SCHEDULER_STOP_SERVICE_TIMEOUT.total_seconds()
72+
),
73+
)

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
from fastapi import FastAPI
33

44
from ...core.settings import ApplicationSettings
5-
from . import _index
5+
from ._router import router
66
from ._utils import set_parent_app
77

88

99
def setup_frontend(app: FastAPI) -> None:
1010
settings: ApplicationSettings = app.state.settings
1111

12-
nicegui.app.include_router(_index.router)
12+
nicegui.app.include_router(router)
1313

1414
nicegui.ui.run_with(
1515
app, mount_path="/", storage_secret=settings.DYNAMIC_SCHEDULER_UI_STORAGE_SECRET

0 commit comments

Comments
 (0)