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 %}