1010from fastui import AnyComponent , FastUI
1111from fastui import components as c
1212from fastui .events import GoToEvent , PageEvent
13+ from models_library .api_schemas_dynamic_scheduler .dynamic_services import (
14+ DynamicServiceStop ,
15+ )
1316from models_library .projects_nodes_io import NodeID
1417from servicelib .background_task import start_periodic_task , stop_periodic_task
1518from servicelib .fastapi .app_state import SingletonInAppStateMixin
1619from servicelib .logging_utils import log_catch , log_context
20+ from servicelib .rabbitmq .rpc_interfaces .dynamic_scheduler .services import (
21+ stop_dynamic_service ,
22+ )
23+ from simcore_service_dynamic_scheduler .services .service_tracker ._models import (
24+ SchedulerServiceState ,
25+ )
1726from starlette import status
1827
28+ from ...core .settings import ApplicationSettings
29+ from ...services .rabbitmq import get_rabbitmq_rpc_client
1930from ...services .service_tracker import (
2031 TrackedServiceModel ,
2132 get_all_tracked_services ,
2233 get_tracked_service ,
2334)
2435from ..dependencies import get_app
2536from . import _custom_components as cu
26- from ._constants import API_ROOT_PATH
37+ from ._constants import API_ROOT_PATH , UI_MOUNT_PREFIX
2738from ._sse_utils import (
2839 AbstractSSERenderer ,
2940 render_items_on_change ,
@@ -51,10 +62,7 @@ def _page_base(
5162 ]
5263
5364
54- @router .get (
55- f"{ API_ROOT_PATH } /" , response_model = FastUI , response_model_exclude_none = True
56- )
57- def api_index () -> list [AnyComponent ]:
65+ def _get_index_page () -> list [AnyComponent ]:
5866 return _page_base (
5967 c .Heading (text = "Dynamic services status" , level = 4 ),
6068 c .Paragraph (text = "List of all services currently tracked by the scheduler" ),
@@ -72,6 +80,13 @@ def api_index() -> list[AnyComponent]:
7280 )
7381
7482
83+ @router .get (
84+ f"{ API_ROOT_PATH } /" , response_model = FastUI , response_model_exclude_none = True
85+ )
86+ def api_index () -> list [AnyComponent ]:
87+ return _get_index_page ()
88+
89+
7590@router .get (
7691 f"{ API_ROOT_PATH } { _PREFIX } /details/" ,
7792 response_model = FastUI ,
@@ -96,42 +111,151 @@ async def service_details(
96111 )
97112
98113
114+ @router .post (
115+ f"{ API_ROOT_PATH } { _PREFIX } /stop-service/" ,
116+ response_model = FastUI ,
117+ response_model_exclude_none = True ,
118+ )
119+ async def stop_service (
120+ node_id : NodeID , app : Annotated [FastAPI , Depends (get_app )]
121+ ) -> list [AnyComponent ]:
122+ service_model = await get_tracked_service (app , node_id )
123+
124+ if service_model and service_model .user_id and service_model .project_id :
125+ settings : ApplicationSettings = app .state .settings
126+ await stop_dynamic_service (
127+ get_rabbitmq_rpc_client (app ),
128+ dynamic_service_stop = DynamicServiceStop (
129+ user_id = service_model .user_id ,
130+ project_id = service_model .project_id ,
131+ node_id = node_id ,
132+ simcore_user_agent = "" ,
133+ save_state = True ,
134+ ),
135+ timeout_s = int (
136+ settings .DYNAMIC_SCHEDULER_STOP_SERVICE_TIMEOUT .total_seconds ()
137+ ),
138+ )
139+
140+ return _get_index_page ()
141+
142+
143+ # TODO: add a toast for displaying the status of the stop operation
144+ # TODO: change logic since SSE is not working as expected. You can use it to detect changes in the model -> then reload the page, which is useless
145+ # TODO: add an endpoint to remove from tracking without using anything else
146+
147+
148+ def _render_partial (
149+ node_id : NodeID , service_model : TrackedServiceModel
150+ ) -> AnyComponent :
151+ list_display : list [tuple [Any , Any ]] = [
152+ ("NodeID" , node_id ),
153+ ("Service state" , service_model .current_state ),
154+ (
155+ "Last state change" ,
156+ arrow .get (service_model .last_state_change ).isoformat (),
157+ ),
158+ ("Requested" , service_model .requested_state ),
159+ ("ProjectID" , service_model .project_id ),
160+ ("UserID" , service_model .user_id ),
161+ ]
162+
163+ if service_model .dynamic_service_start :
164+ list_display .extend (
165+ [
166+ ("Service Key" , service_model .dynamic_service_start .key ),
167+ ("Service Version" , service_model .dynamic_service_start .version ),
168+ ("Product" , service_model .dynamic_service_start .product_name ),
169+ ]
170+ )
171+ components = [
172+ c .Text (text = "PARTIAL" ),
173+ cu .markdown_list_display (list_display ),
174+ c .Button (
175+ text = "Details" ,
176+ named_style = "secondary" ,
177+ on_click = GoToEvent (url = f"{ _PREFIX } /details/?node_id={ node_id } " ),
178+ ),
179+ c .Button (
180+ text = "Stop Service" ,
181+ on_click = PageEvent (name = "modal-prompt" ),
182+ class_name = "+ ms-2" ,
183+ ),
184+ ]
185+
186+ return c .Div (components = components , class_name = "border border-double" )
187+
188+
189+ def _render_full (node_id : NodeID , service_model : TrackedServiceModel ) -> AnyComponent :
190+ list_display : list [tuple [Any , Any ]] = [
191+ ("NodeID" , node_id ),
192+ ("Service state" , service_model .current_state ),
193+ (
194+ "Last state change" ,
195+ arrow .get (service_model .last_state_change ).isoformat (),
196+ ),
197+ ("Requested" , service_model .requested_state ),
198+ ("ProjectID" , service_model .project_id ),
199+ ("UserID" , service_model .user_id ),
200+ ]
201+
202+ if service_model .dynamic_service_start :
203+ list_display .extend (
204+ [
205+ ("Service Key" , service_model .dynamic_service_start .key ),
206+ ("Service Version" , service_model .dynamic_service_start .version ),
207+ ("Product" , service_model .dynamic_service_start .product_name ),
208+ ]
209+ )
210+ components = [
211+ c .Text (text = "FULL_RENDERED" ),
212+ cu .markdown_list_display (list_display ),
213+ c .Button (
214+ text = "Details" ,
215+ named_style = "secondary" ,
216+ on_click = GoToEvent (url = f"{ _PREFIX } /details/?node_id={ node_id } " ),
217+ ),
218+ c .Button (
219+ text = "Stop Service" ,
220+ on_click = PageEvent (name = "modal-prompt" ),
221+ class_name = "+ ms-2" ,
222+ ),
223+ c .Modal (
224+ title = "Stop Service" ,
225+ body = [
226+ c .Paragraph (text = f"Are you sure you want to stop { node_id } ?" ),
227+ c .Form (
228+ form_fields = [],
229+ submit_url = f"{ UI_MOUNT_PREFIX } { API_ROOT_PATH } { _PREFIX } /stop-service/?node_id={ node_id } " ,
230+ loading = [c .Spinner (text = "Stopping..." )],
231+ footer = [],
232+ submit_trigger = PageEvent (name = "modal-form-submit" ),
233+ ),
234+ ],
235+ footer = [
236+ c .Button (
237+ text = "Cancel" ,
238+ named_style = "secondary" ,
239+ on_click = PageEvent (name = "modal-prompt" , clear = True ),
240+ ),
241+ c .Button (text = "Submit" , on_click = PageEvent (name = "modal-form-submit" )),
242+ ],
243+ open_trigger = PageEvent (name = "modal-prompt" ),
244+ ),
245+ ]
246+
247+ return c .Div (components = components , class_name = "border border-dotted" )
248+
249+
99250class ServicesSSERenderer (AbstractSSERenderer ):
100251 @staticmethod
101252 def get_component (item : tuple [NodeID , TrackedServiceModel ]) -> AnyComponent :
102253 node_id , service_model = item
103254
104- list_display : list [tuple [Any , Any ]] = [
105- ("NodeID" , node_id ),
106- ("Service state" , service_model .current_state ),
107- (
108- "Last state change" ,
109- arrow .get (service_model .last_state_change ).isoformat (),
110- ),
111- ("Requested" , service_model .requested_state ),
112- ("ProjectID" , service_model .project_id ),
113- ("UserID" , service_model .user_id ),
114- ]
115-
116- if service_model .dynamic_service_start :
117- list_display .extend (
118- [
119- ("Service Key" , service_model .dynamic_service_start .key ),
120- ("Service Version" , service_model .dynamic_service_start .version ),
121- ("Product" , service_model .dynamic_service_start .product_name ),
122- ]
123- )
124- components = [
125- cu .markdown_list_display (list_display ),
126- c .Link (
127- components = [c .Text (text = "Details" )],
128- on_click = GoToEvent (
129- url = f"{ _PREFIX } /details/?node_id={ node_id } " ,
130- ),
131- ),
132- ]
255+ if service_model .current_state == SchedulerServiceState .RUNNING :
256+ return _render_full (node_id , service_model )
133257
134- return c . Div ( components = components , class_name = "border border-blue-500 px-4" )
258+ return _render_partial ( node_id , service_model )
135259
136260
137261@router .get (f"{ API_ROOT_PATH } { _PREFIX } /sse/" )
@@ -173,7 +297,7 @@ def startup(self) -> None:
173297 self ._task = start_periodic_task (
174298 self ._task_service_state_retrieval ,
175299 interval = self .poll_interval ,
176- task_name = "sse_periodic_status_poll " ,
300+ task_name = "sse_services_status_retrieval_from_redis " ,
177301 )
178302
179303 async def shutdown (self ) -> None :
0 commit comments