Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 78 additions & 10 deletions src/blueapi/service/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import logging
import urllib.parse
from collections.abc import Awaitable, Callable
Expand All @@ -11,15 +12,22 @@
Body,
Depends,
FastAPI,
Form,
HTTPException,
Request,
Response,
status,
)
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,
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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 = "<ixx>"

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="/")
135 changes: 135 additions & 0 deletions templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg" href="/favicon">
<title>{{instrument}}-blueapi</title>
<style>
dl {
padding-left: 30px;
}
.container {
border: 2px solid rgb(75 70 74);
border-radius: 0.5em;
padding: 20px 10px;
font: sans-serif;

display: flex;
}
.container > * {
padding: 10px;
border: 2px solid rgb(95 97 110);
border-radius: 0.5em;

margin: 0 10px;
flex: 1 1 200px;
}
</style>
</head>
<body>
<h1>{{instrument}}-blueapi</h1>
<p>
api docs available at
<a href="/docs">/docs</a>.
</p>

<div class="container">

<div>
<h2>Run Plan</h2>
<form action="/run" method="POST" id="run_form">
<div>
<label for="instrument_session">Instrument Session</label>
<input type="text" id="instrument_session" name="instrument_session" placeholder="cm1234-5" required/>
</div>

<div>
<label for="name">Select Plan</label>
<select id="name" name="name", form="run_form" required>
<option value="">--select a plan--</option>
{% for plan in plans %}
<option value="{{plan.name}}">{{plan.name}}</option>
{% endfor %}
</select>
</div>

<div>
<textarea id="params" name="params" placeholder="Plan parameters" rows='10' cols='60'></textarea>
</div>

<input type="submit" value="run">
</form>

<h2>Tasks</h2>

<h3>Current task:</h3>
{% for task in tasks if not task.is_pending and not task.is_complete %}
<dl>
<dt> {{task.task_id}}
<ul>
<li>{{task.task.name}}</li>
<li>{{task.task.params}}</li>
</ul>
</dt>
</dl>
{% endfor %}

<h3>Pending tasks:</h3>
{% for task in tasks if task.is_pending %}
<dl>
<dt> {{task.task_id}}
<ul>
<li>{{task.task.name}}</li>
<li>{{task.task.params}}</li>
</ul>
</dt>
</dl>
{% endfor %}

<h3>Completed tasks:</h3>
{% for task in tasks if task.is_complete%}
<dl>
<dt> {{task.task_id}}
<ul>
<li>{{task.task.name}}</li>
<li>{{task.task.params}}</li>
<li>outcome: {{task.outcome.outcome}} </li>
<li>result: {{task.outcome.result}}</li>
</ul>
</dt>
</dl>
{% endfor %}

</div>

<div>
<h2>Plans</h2>
{% for plan in plans %}
<h3>{{plan.name}}</h3>
<dl>
<dt>Description:</dt>
<dd>{{plan.description | replace("\n", "<br>") | safe }}</dd>
<dt> Plan Parameters:</dt>
{% for p in plan.parameter_schema.properties %}
<dd>{{ p }}</dd>
{% endfor %}
</dl>
{% endfor %}
</div>

<div>
<h2>Devices</h2>
{% for device in devices %}
<h3> {{device.device}}</h3>
<dl>
<dt>Protocols:</dt>
<dd>{{device.protocols}}</dd>
</dl>
{% endfor %}
</div>

</div>

</body>
</html>
Loading