From ae0c0d1cccf5064c8b1b45a3bbc63b7e84bd1efb Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Tue, 12 Dec 2023 15:32:55 +0100 Subject: [PATCH] Initial commit of monitor api --- ocrdmonitor/database/_ocrdjobrepository.py | 10 +- ocrdmonitor/ocrdcontroller.py | 14 +- ocrdmonitor/protocols.py | 33 +++- ocrdmonitor/server/api/__init__.py | 17 ++ .../style.css => api/routers/__init__.py} | 0 ocrdmonitor/server/api/routers/jobs.py | 45 +++++ ocrdmonitor/server/api/routers/workspaces.py | 30 +++ ocrdmonitor/server/app.py | 2 + ocrdmonitor/server/jobs.py | 5 +- ocrdmonitor/server/static/main.css | 32 +++ ocrdmonitor/server/static/main.js | 73 +++++++ ocrdmonitor/server/templates/base.html.j2 | 23 ++- ocrdmonitor/server/templates/list.html.j2 | 35 ++++ ocrdmonitor/server/templates/table.html.j2 | 185 ++++++++++++++++++ ocrdmonitor/server/workspaces/__init__.py | 2 +- ocrdmonitor/server/workspaces/_listroutes.py | 17 +- 16 files changed, 485 insertions(+), 38 deletions(-) create mode 100644 ocrdmonitor/server/api/__init__.py rename ocrdmonitor/server/{static/style.css => api/routers/__init__.py} (100%) create mode 100644 ocrdmonitor/server/api/routers/jobs.py create mode 100644 ocrdmonitor/server/api/routers/workspaces.py create mode 100644 ocrdmonitor/server/static/main.css create mode 100644 ocrdmonitor/server/static/main.js create mode 100644 ocrdmonitor/server/templates/list.html.j2 create mode 100644 ocrdmonitor/server/templates/table.html.j2 diff --git a/ocrdmonitor/database/_ocrdjobrepository.py b/ocrdmonitor/database/_ocrdjobrepository.py index dd07d35..77ddc11 100644 --- a/ocrdmonitor/database/_ocrdjobrepository.py +++ b/ocrdmonitor/database/_ocrdjobrepository.py @@ -39,4 +39,12 @@ async def insert(self, job: OcrdJob) -> None: await MongoOcrdJob(**asdict(job)).insert() # type: ignore async def find_all(self) -> list[OcrdJob]: - return [OcrdJob(**j.dict(exclude={"id"})) for j in await MongoOcrdJob.find_all().to_list()] + return [ + OcrdJob(**j.dict()) + for j in await MongoOcrdJob.find_all() + .sort(-MongoOcrdJob.time_created) + .to_list() + ] + + async def get(self, id: str) -> OcrdJob: + return await MongoOcrdJob.get(id) diff --git a/ocrdmonitor/ocrdcontroller.py b/ocrdmonitor/ocrdcontroller.py index f869946..5721c21 100644 --- a/ocrdmonitor/ocrdcontroller.py +++ b/ocrdmonitor/ocrdcontroller.py @@ -13,13 +13,15 @@ async def status_for(self, ocrd_job: OcrdJob) -> ProcessStatus | None: return None pid = await self._remote.read_file(f"/data/{ocrd_job.remotedir}/ocrd.pid") - process_statuses = await self._remote.process_status(int(pid)) - for status in process_statuses: - if status.state == ProcessState.RUNNING: - return status + if pid : + process_statuses = await self._remote.process_status(int(pid)) - if process_statuses: - return process_statuses[0] + for status in process_statuses: + if status.state == ProcessState.RUNNING: + return status + + if process_statuses: + return process_statuses[0] return None diff --git a/ocrdmonitor/protocols.py b/ocrdmonitor/protocols.py index 066c0bf..016514c 100644 --- a/ocrdmonitor/protocols.py +++ b/ocrdmonitor/protocols.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass from datetime import datetime from pathlib import Path from typing import Collection, NamedTuple, Protocol @@ -7,6 +6,7 @@ from ocrdmonitor.processstatus import ProcessStatus from ocrdmonitor.server.settings import Settings +from pydantic import BaseModel, computed_field class BrowserRestoringFactory(Protocol): def __call__( @@ -37,12 +37,12 @@ async def count(self) -> int: ... -@dataclass(frozen=True) -class OcrdJob: +class OcrdJob(BaseModel): + id: str pid: int | None return_code: int | None time_created: datetime - time_terminated: datetime + time_terminated: datetime | None process_id: str task_id: str process_dir: Path @@ -52,23 +52,44 @@ class OcrdJob: controller_address: str @property - def is_running(self) -> bool: + def is_processing(self) -> bool: return self.pid is not None @property def is_completed(self) -> bool: return self.return_code is not None + @computed_field @property def workflow(self) -> str: return Path(self.workflow_file).name + + @computed_field + @property + def workspace(self) -> str: + return Path(self.process_dir).name + + @computed_field + @property + def status(self) -> str: + if self.is_processing : + return "PROCESSING" + if self.is_completed : + if self.return_code == 0 : + return "SUCCESS" + else : + return "FAILURE" + return "UNDEFINED" class JobRepository(Protocol): async def insert(self, job: OcrdJob) -> None: ... - async def find_all(self) -> list[OcrdJob]: + async def find_one(self) -> list[OcrdJob]: + ... + + async def get(self) -> OcrdJob: ... diff --git a/ocrdmonitor/server/api/__init__.py b/ocrdmonitor/server/api/__init__.py new file mode 100644 index 0000000..ff7bf03 --- /dev/null +++ b/ocrdmonitor/server/api/__init__.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from fastapi import APIRouter + +from ocrdmonitor.protocols import Environment + +from .routers import workspaces +from .routers import jobs + +def create_api( + environment: Environment +) -> APIRouter: + router = APIRouter(prefix="/api") + + router.include_router(workspaces.router(browser_settings=environment.settings.ocrd_browser)) + router.include_router(jobs.router(environment)) + return router diff --git a/ocrdmonitor/server/static/style.css b/ocrdmonitor/server/api/routers/__init__.py similarity index 100% rename from ocrdmonitor/server/static/style.css rename to ocrdmonitor/server/api/routers/__init__.py diff --git a/ocrdmonitor/server/api/routers/jobs.py b/ocrdmonitor/server/api/routers/jobs.py new file mode 100644 index 0000000..8266200 --- /dev/null +++ b/ocrdmonitor/server/api/routers/jobs.py @@ -0,0 +1,45 @@ +from fastapi import APIRouter, Response, Depends + +from ocrdmonitor.protocols import Environment, OcrdJob, Repositories + +from typing import List + +from ocrdmonitor.ocrdcontroller import OcrdController + +class ResultList: + def __init__(self, results: List[OcrdJob]): + self.results = results + + +def router(environment: Environment) -> None: + router = APIRouter(prefix="/jobs") + + @router.get("/", name="api.jobs") + async def jobs( + repositories: Repositories = Depends(environment.repositories), + ) -> Response: + job_repository = repositories.ocrd_jobs + jobs = await job_repository.find_all() + + return ResultList(jobs) + + @router.get("/{job_id}", name="api.job") + async def job( + repositories: Repositories = Depends(environment.repositories), + ) -> Response: + job_repository = repositories.ocrd_jobs + jobs = await job_repository.find_all() + + return ResultList(jobs) + + @router.get("/{job_id}/processstatus", name="api.job.processstatus") + async def job_processstatus( + job_id: str, repositories: Repositories = Depends(environment.repositories) + ) -> Response: + controller = OcrdController(environment.controller_server()) + + job_repository = repositories.ocrd_jobs + job = await job_repository.get(job_id) + return await controller.status_for(job) + + return router diff --git a/ocrdmonitor/server/api/routers/workspaces.py b/ocrdmonitor/server/api/routers/workspaces.py new file mode 100644 index 0000000..baa56e2 --- /dev/null +++ b/ocrdmonitor/server/api/routers/workspaces.py @@ -0,0 +1,30 @@ +from pathlib import Path + +from fastapi import APIRouter, Response +from ocrdbrowser import workspace +from ocrdmonitor.server.settings import OcrdBrowserSettings + +from typing import List + +class ResultList(): + def __init__(self, results: List[Path]): + self.results = results + +def router( + browser_settings: OcrdBrowserSettings +) -> None: + router = APIRouter(prefix="/workspaces") + + @router.get("/", name="api.list.workspaces") + def list_workspaces(search: str | None = None) -> Response: + spaces = [ + Path(space).relative_to(browser_settings.workspace_dir) + for space in workspace.list_all(browser_settings.workspace_dir) + ] + + if search: + spaces = list(filter(lambda workspace: search in str(workspace), spaces)) + + return ResultList(spaces) + + return router \ No newline at end of file diff --git a/ocrdmonitor/server/app.py b/ocrdmonitor/server/app.py index 7e45698..954f110 100644 --- a/ocrdmonitor/server/app.py +++ b/ocrdmonitor/server/app.py @@ -9,6 +9,7 @@ from fastapi.templating import Jinja2Templates from ocrdmonitor.protocols import Environment +from ocrdmonitor.server.api import create_api from ocrdmonitor.server.index import create_index from ocrdmonitor.server.jobs import create_jobs from ocrdmonitor.server.lifespan import lifespan @@ -45,6 +46,7 @@ async def validation_exception( content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}), ) + app.include_router(create_api(environment)) app.include_router(create_index(templates)) app.include_router(create_jobs(templates, environment)) app.include_router(create_workspaces(templates, environment)) diff --git a/ocrdmonitor/server/jobs.py b/ocrdmonitor/server/jobs.py index fe838d2..d043f79 100644 --- a/ocrdmonitor/server/jobs.py +++ b/ocrdmonitor/server/jobs.py @@ -25,7 +25,7 @@ class RunningJob: def split_into_running_and_completed( jobs: Iterable[OcrdJob], ) -> tuple[list[OcrdJob], list[OcrdJob]]: - running_ocrd_jobs = [job for job in jobs if job.is_running] + running_ocrd_jobs = [job for job in jobs if job.is_processing] completed_ocrd_jobs = [job for job in jobs if job.is_completed] return running_ocrd_jobs, completed_ocrd_jobs @@ -63,9 +63,10 @@ async def jobs( now = datetime.now(timezone.utc) return templates.TemplateResponse( - "jobs.html.j2", + "table.html.j2", { "request": request, + "title": "Jobs", "running_jobs": sorted( running_jobs, key=lambda x: x.ocrd_job.time_created or now, diff --git a/ocrdmonitor/server/static/main.css b/ocrdmonitor/server/static/main.css new file mode 100644 index 0000000..c41b03b --- /dev/null +++ b/ocrdmonitor/server/static/main.css @@ -0,0 +1,32 @@ +#loader { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + opacity: 0; + z-index: -1; + transition: opacity .3s; + display: flex; + justify-content: center; + align-items: center; +} + +#loader.is-active { + opacity: 1; + z-index: 1; +} + +#loader .is-loading { + position: relative; +} + +#loader .loader { + height: 40px; + width: 40px; +} + +.ocrd-status { + width: 40px; + border: 0px; +} \ No newline at end of file diff --git a/ocrdmonitor/server/static/main.js b/ocrdmonitor/server/static/main.js new file mode 100644 index 0000000..e0e41d1 --- /dev/null +++ b/ocrdmonitor/server/static/main.js @@ -0,0 +1,73 @@ +class ResultsView { + constructor( container, renderResultCallback, afterRenderCallback = false ) { + this.container = container; + this.renderResultCallback = renderResultCallback; + this.loader = this.initLoader() + this.afterRenderCallback = afterRenderCallback + } + + async render(url) { + this.showLoader(); + // Storing response + const response = await fetch(url); + + // Storing data in form of JSON + let data = await response.json(); + console.log(data); + + this.show(data); + + if( this.afterRenderCallback ) { + this.afterRenderCallback() + } + + this.hideLoader(); + } + + show(data) { + let content = "" + + if(!data.results || data.results.length == 0) { + content = "No results were found" + } else { + for (let result of data.results) { + content += this.renderResultCallback(result) + } + } + + this.container.innerHTML = content; + } + + hideLoader() { + this.loader.classList.remove("is-active"); + } + + showLoader() { + this.loader.classList.add("is-active"); + } + + initLoader() { + let loader = document.createElement('div'); + loader.id = 'loader'; + loader.innerHTML = '
' + let loaderContainer = this.container + if(loaderContainer.tagName == 'TBODY') { + loaderContainer = loaderContainer.parentNode + } + + loaderContainer.parentNode.insertBefore( loader, loaderContainer); + return document.getElementById('loader'); + } + +} + + +function diff(time_created, time_terminated) { + let from = moment.utc(time_created) + let to = moment.utc() + if( time_terminated ) { + to = moment.utc(time_terminated) + } + + return moment.utc(to.diff(from)).format('HH:mm:ss') +} diff --git a/ocrdmonitor/server/templates/base.html.j2 b/ocrdmonitor/server/templates/base.html.j2 index 85012b3..40e3dfb 100644 --- a/ocrdmonitor/server/templates/base.html.j2 +++ b/ocrdmonitor/server/templates/base.html.j2 @@ -6,7 +6,10 @@ {% block meta %}{% endblock %} {% block title %}{% endblock %} - OCR-D Monitor - + + + + -
-
-

- {% block headline %}{% endblock %} -

- {% block content %}{% endblock %} -
-
+ +
+
+

+ {% block headline %}{% endblock %} +

+ {% block content %}{% endblock %} +
+
diff --git a/ocrdmonitor/server/templates/list.html.j2 b/ocrdmonitor/server/templates/list.html.j2 new file mode 100644 index 0000000..4b9aaea --- /dev/null +++ b/ocrdmonitor/server/templates/list.html.j2 @@ -0,0 +1,35 @@ +{% extends 'base.html.j2' %} + +{% block headline %} + {% block title %}{{ title }}{% endblock %} +{% endblock %} +{% block content %} + +
+
+ +
+
+ +
+
+
+ + + + + +{% endblock %} diff --git a/ocrdmonitor/server/templates/table.html.j2 b/ocrdmonitor/server/templates/table.html.j2 new file mode 100644 index 0000000..8a29ad3 --- /dev/null +++ b/ocrdmonitor/server/templates/table.html.j2 @@ -0,0 +1,185 @@ +{% extends 'base.html.j2' %} + +{% block headline %} +{% block title %}{{ title }}{% endblock %} +{% endblock %} +{% block content %} + +
+
+ +
+
+ +
+
+
+ + + +

Active Jobs

+ + + + + + + + + + + + + + + + + {% for job in running_jobs: %} + + + + + + + + + + + + + {% endfor %} + +
TSTARTTASK IDPROCESS IDWORKFLOWPIDSTATUS% CPUMB RSSDURATIONACTION
{{ job.ocrd_job.time_created }}{{ job.ocrd_job.task_id }}{{ job.ocrd_job.process_id }}{{ job.ocrd_job.workflow + }}{{ job.process_status.pid }}{{ job.process_status.state }}{{ job.process_status.percent_cpu }}{{ job.process_status.memory }}{{ job.process_status.cpu_time }} +
+

Inactive Jobs

+ + + + + + + + + + + + + + {% for job in completed_jobs: %} + + + + + + + + + + {% endfor %} + +
TSTOPTASK IDPROCESS IDWORKFLOWRETVALWORKSPACELOGS
{{ job.time_terminated }}{{ job.task_id }}{{ job.process_id }}{{ job.workflow }}{{ job.return_code }} {% if job.return_code == 0 %}(SUCCESS){% else %}(FAILURE){% endif %}{{ job.process_dir.name }} + ocrd.log +
+ + +{% endblock %} \ No newline at end of file diff --git a/ocrdmonitor/server/workspaces/__init__.py b/ocrdmonitor/server/workspaces/__init__.py index f5a2b37..d33f1f5 100644 --- a/ocrdmonitor/server/workspaces/__init__.py +++ b/ocrdmonitor/server/workspaces/__init__.py @@ -29,7 +29,7 @@ async def get_browser_repository() -> BrowserProcessRepository: browser_repository = Depends(get_browser_repository) browser_factory = Depends(environment.browser_factory) - register_listroutes(router, templates, browser_settings) + register_listroutes(router, templates) register_launchroutes( router, templates, browser_factory, browser_repository, full_workspace ) diff --git a/ocrdmonitor/server/workspaces/_listroutes.py b/ocrdmonitor/server/workspaces/_listroutes.py index b2dbd5e..baabff5 100644 --- a/ocrdmonitor/server/workspaces/_listroutes.py +++ b/ocrdmonitor/server/workspaces/_listroutes.py @@ -3,21 +3,14 @@ from fastapi import APIRouter, Request, Response from fastapi.templating import Jinja2Templates -from ocrdbrowser import workspace -from ocrdmonitor.server.settings import OcrdBrowserSettings - - def register_listroutes( - router: APIRouter, templates: Jinja2Templates, browser_settings: OcrdBrowserSettings + router: APIRouter, templates: Jinja2Templates ) -> None: + @router.get("/", name="workspaces.list") def list_workspaces(request: Request) -> Response: - spaces = [ - Path(space).relative_to(browser_settings.workspace_dir) - for space in workspace.list_all(browser_settings.workspace_dir) - ] - return templates.TemplateResponse( - "list_workspaces.html.j2", - {"request": request, "workspaces": spaces}, + "list.html.j2", + {"request": request, "title": "Workspaces"}, ) + \ No newline at end of file