diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index 679119238..6bb6a5e78 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -1,3 +1,4 @@ +import json import logging import urllib.parse from collections.abc import Awaitable, Callable @@ -11,6 +12,7 @@ Body, Depends, FastAPI, + Form, HTTPException, Request, Response, @@ -18,8 +20,14 @@ ) from fastapi.datastructures import Address from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import RedirectResponse, StreamingResponse +from fastapi.responses import ( + FileResponse, + HTMLResponse, + RedirectResponse, + StreamingResponse, +) from fastapi.security import OAuth2AuthorizationCodeBearer +from fastapi.templating import Jinja2Templates from observability_utils.tracing import ( add_span_attributes, get_tracer, @@ -181,15 +189,6 @@ async def on_token_error_401(_: Request, __: Exception): ) -@secure_router.get("/", include_in_schema=False) -def root_redirect() -> RedirectResponse: - """Redirect to docs url""" - return RedirectResponse( - status_code=status.HTTP_307_TEMPORARY_REDIRECT, - url=ApplicationConfig.DOCS_ENDPOINT, - ) - - @secure_router_v1.get("/environment", tags=[Tag.ENV]) @secure_router.get("/environment", tags=[Tag.ENV]) @start_as_current_span(TRACER, "runner") @@ -659,3 +658,72 @@ async def inject_propagated_observability_context( attach(ctx) response = await call_next(request) return response + + +templates = Jinja2Templates(directory="templates") + + +@secure_router.get("/", include_in_schema=False, response_class=HTMLResponse) +def root_landing( + request: Request, + runner: Annotated[WorkerDispatcher, Depends(_runner)], +) -> HTMLResponse: + + if runner._config.env.metadata: + instrument = runner._config.env.metadata.instrument + else: + instrument = "" + + devices = runner.run(interface.get_devices) + devices = [ + {"device": device.name, "protocols": [p.name for p in device.protocols]} + for device in devices + ] + + plans = runner.run(interface.get_plans) + task_list = get_tasks(runner) + + context = { + "instrument": instrument, + "devices": devices, + "plans": plans, + "tasks": task_list.tasks, + } + + return templates.TemplateResponse( + request=request, name="index.html", context=context + ) + + +@open_router.get("/favicon", include_in_schema=False) +async def favicon(): + return FileResponse("docs/images/blueapi-logo.svg") + + +@secure_router_v1.post("/run", include_in_schema=True, tags=[Tag.TASK]) +@start_as_current_span(TRACER) +def run( + name: Annotated[str, Form()], + params: Annotated[str, Form()], + instrument_session: Annotated[str, Form()], + request: Request, + response: Response, + runner: Annotated[WorkerDispatcher, Depends(_runner)], +) -> RedirectResponse: + + task_request = TaskRequest( + name=name, + params=json.loads(params), # do this validator in the model? + instrument_session=instrument_session, + ) + res = submit_task(request, response, task_request, runner) + + tid = res.task_id + req_task = WorkerTask(task_id=tid) + + try: + set_active_task(request, req_task, runner) + except HTTPException: + delete_submitted_task(tid, runner) + + return RedirectResponse(status_code=status.HTTP_204_NO_CONTENT, url="/") diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 000000000..ecd366630 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,135 @@ + + + + + + + {{instrument}}-blueapi + + + +

{{instrument}}-blueapi

+

+ api docs available at + /docs. +

+ +
+ +
+

Run Plan

+
+
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +

Tasks

+ +

Current task:

+ {% for task in tasks if not task.is_pending and not task.is_complete %} +
+
{{task.task_id}} +
    +
  • {{task.task.name}}
  • +
  • {{task.task.params}}
  • +
+
+
+ {% endfor %} + +

Pending tasks:

+ {% for task in tasks if task.is_pending %} +
+
{{task.task_id}} +
    +
  • {{task.task.name}}
  • +
  • {{task.task.params}}
  • +
+
+
+ {% endfor %} + +

Completed tasks:

+ {% for task in tasks if task.is_complete%} +
+
{{task.task_id}} +
    +
  • {{task.task.name}}
  • +
  • {{task.task.params}}
  • +
  • outcome: {{task.outcome.outcome}}
  • +
  • result: {{task.outcome.result}}
  • +
+
+
+ {% endfor %} + +
+ +
+

Plans

+ {% for plan in plans %} +

{{plan.name}}

+
+
Description:
+
{{plan.description | replace("\n", "
") | safe }}
+
Plan Parameters:
+ {% for p in plan.parameter_schema.properties %} +
{{ p }}
+ {% endfor %} +
+ {% endfor %} +
+ +
+

Devices

+ {% for device in devices %} +

{{device.device}}

+
+
Protocols:
+
{{device.protocols}}
+
+ {% endfor %} +
+ +
+ + +