|
| 1 | +import json |
| 2 | + |
| 3 | +import arrow |
| 4 | +import httpx |
1 | 5 | from fastapi import FastAPI |
2 | 6 | from models_library.projects_nodes_io import NodeID |
3 | 7 | from nicegui import APIRouter, app, ui |
4 | 8 | from nicegui.element import Element |
| 9 | +from settings_library.utils_service import DEFAULT_FASTAPI_PORT |
5 | 10 |
|
6 | 11 | from ...services.service_tracker import TrackedServiceModel, get_all_tracked_services |
| 12 | +from ...services.service_tracker._models import SchedulerServiceState |
7 | 13 | from ._common import base_page |
8 | 14 | from ._utils import get_parent_app |
9 | 15 |
|
10 | 16 | router = APIRouter() |
11 | 17 |
|
12 | 18 |
|
| 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 | + |
13 | 106 | def _render_card( |
14 | 107 | card_container: Element, node_id: NodeID, service: TrackedServiceModel |
15 | 108 | ) -> None: |
16 | 109 | 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) |
21 | 113 |
|
22 | 114 |
|
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 |
25 | 119 |
|
26 | | - tracked_services = await get_all_tracked_services(parent_app) |
| 120 | + async def update(self) -> None: |
| 121 | + # TODO: rerender only if data changed |
27 | 122 |
|
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 |
30 | 124 |
|
| 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) |
31 | 129 |
|
32 | | -# changed this form ui to router |
33 | | -@router.page("/") |
34 | | -async def main_page(): |
35 | | - parent_app = get_parent_app(app) |
36 | 130 |
|
| 131 | +@router.page("/") |
| 132 | +async def index(): |
37 | 133 | with base_page(): |
38 | 134 |
|
39 | 135 | # Initial UI setup |
40 | 136 | ui.label("Dynamic Item List") |
41 | 137 |
|
42 | 138 | card_container: Element = ui.row() |
43 | 139 |
|
44 | | - # render cards when page is loaded |
45 | | - await _update_cards(parent_app, card_container) |
| 140 | + updater = CardUpdater(get_parent_app(app), card_container) |
46 | 141 |
|
| 142 | + # render cards when page is loaded |
| 143 | + await updater.update() |
47 | 144 | # 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()) |
0 commit comments