Skip to content

Commit

Permalink
Job details page, part 1 (#71)
Browse files Browse the repository at this point in the history
Adding the basic information for the Job details page, and a button to access it on the list jobs page.

Additionally:

    Added an endpoint to get the job details given its ID.
    Renamed the endpoint to get status to be under job/status/{status} so the GET method under /job/{id} returns the job by id.
    Moved some front end definitions to their own file so they can be better shared between pages.

Progress component and error coming on follow up PRs.
  • Loading branch information
lotif authored Aug 7, 2024
1 parent a9f9072 commit 992ca66
Show file tree
Hide file tree
Showing 11 changed files with 652 additions and 35 deletions.
31 changes: 29 additions & 2 deletions florist/api/routes/server/job.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""FastAPI routes for the job."""

from typing import List
from typing import List, Union

from fastapi import APIRouter, Body, Request, status
from fastapi.responses import JSONResponse
Expand All @@ -11,6 +11,29 @@
router = APIRouter()


@router.get(
path="/{job_id}",
response_description="Retrieves a job by ID",
status_code=status.HTTP_200_OK,
response_model=Job,
)
async def get_job(job_id: str, request: Request) -> Union[Job, JSONResponse]:
"""
Retrieve a training job by its ID.
:param request: (fastapi.Request) the FastAPI request object.
:param job_id: (str) The ID of the job to be retrieved.
:return: (Union[Job, JSONResponse]) The job with the given ID, or a 400 JSONResponse if it hasn't been found.
"""
job = await Job.find_by_id(job_id, request.app.database)

if job is None:
return JSONResponse(content={"error": f"Job with ID {job_id} does not exist."}, status_code=400)

return job


@router.post(
path="",
response_description="Create a new job",
Expand All @@ -36,7 +59,11 @@ async def new_job(request: Request, job: Job = Body(...)) -> Job: # noqa: B008
return job_in_db


@router.get(path="/{status}", response_description="List jobs with the specified status", response_model=List[Job])
@router.get(
path="/status/{status}",
response_description="List jobs with the specified status",
response_model=List[Job],
)
async def list_jobs_with_status(status: JobStatus, request: Request) -> List[Job]:
"""
List jobs with specified status.
Expand Down
8 changes: 8 additions & 0 deletions florist/app/assets/css/florist.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,11 @@
.save-btn {
width: 100px;
}

.status-pill {
display: inline-flex;
align-items: center;
padding: 5px 10px;
border-radius: 5px;
color: white;
}
26 changes: 26 additions & 0 deletions florist/app/jobs/definitions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Must be in same order as array returned from useGetJobsByJobStatus
export const validStatuses = {
NOT_STARTED: "Not Started",
IN_PROGRESS: "In Progress",
FINISHED_SUCCESSFULLY: "Finished Successfully",
FINISHED_WITH_ERROR: "Finished with Error",
};

export interface JobData {
_id: string;
status: string;
model: string;
server_address: string;
server_info: string;
redis_host: string;
redis_port: string;
clients_info: Array<ClientInfo>;
}

export interface ClientInfo {
client: string;
service_address: string;
data_path: string;
redis_host: string;
redis_port: string;
}
288 changes: 288 additions & 0 deletions florist/app/jobs/details/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
"use client";

import { useSearchParams } from "next/navigation";
import Image from "next/image";

import { ReactElement } from "react/React";

import { useGetJob } from "../hooks";
import { validStatuses, ClientInfo } from "../definitions";
import loading_gif from "../../assets/img/loading.gif";

export default function JobDetails(): ReactElement {
return (
<div className="col-12 align-items-center">
<JobDetailsHeader />
<JobDetailsBody />
</div>
);
}

export function JobDetailsHeader(): ReactElement {
return (
<div className="card-header pb-0 pt-3">
<h1 className="mt-3 mb-0">Job Details</h1>
</div>
);
}

export function JobDetailsBody(): ReactElement {
const searchParams = useSearchParams();
const jobId = searchParams.get("id");

const { data: job, error, isLoading } = useGetJob(jobId);

if (isLoading) {
return (
<div className="d-flex justify-content-center align-items-center vh-100">
<Image id="job-details-loading" src={loading_gif} alt="Loading" height={64} width={64} />
</div>
);
}

if (!job || error) {
if (error) {
console.error(error);
}
return (
<div className="container pt-3 p-0">
<div className="alert alert-danger text-white" id="job-details-error">
Error retrieving job.
</div>
</div>
);
}

return (
<div className="container pt-3 p-0">
<div className="row pb-2">
<div className="col-sm-2">
<strong className="text-dark">Job ID:</strong>
</div>
<div className="col-sm" id="job-details-id">
{job._id}
</div>
</div>
<div className="row pb-2">
<div className="col-sm-2 align-content-center">
<strong className="text-dark">Status:</strong>
</div>
<div className="col-sm">
<JobDetailsStatus status={job.status} />
</div>
</div>
<div className="row pb-2">
<div className="col-sm-2">
<strong className="text-dark">Model:</strong>
</div>
<div className="col-sm" id="job-details-model">
{job.model}
</div>
</div>
<div className="row pb-2">
<div className="col-sm-2">
<strong className="text-dark">Server Address:</strong>
</div>
<div className="col-sm" id="job-details-server-address">
{job.server_address}
</div>
</div>
<div className="row pb-2">
<div className="col-sm-2">
<strong className="text-dark">Redis Host:</strong>
</div>
<div className="col-sm" id="job-details-redis-host">
{job.redis_host}
</div>
</div>
<div className="row pb-2 mb-4">
<div className="col-sm-2">
<strong className="text-dark">Redis Port:</strong>
</div>
<div className="col-sm" id="job-details-redis-port">
{job.redis_port}
</div>
</div>

<JobDetailsTable
Component={JobDetailsServerConfigTable}
title="Server Configuration"
data={job.server_config}
/>

<JobDetailsTable
Component={JobDetailsClientsInfoTable}
title="Clients Configuration"
data={job.clients_info}
/>
</div>
);
}

export function JobDetailsStatus({ status }: { status: string }): ReactElement {
let pillClasses = "status-pill text-sm ";
let iconName;
let statusDescription;
switch (String(validStatuses[status])) {
case validStatuses.NOT_STARTED:
pillClasses += "alert-info";
iconName = "radio_button_checked";
statusDescription = validStatuses[status];
break;
case validStatuses.IN_PROGRESS:
pillClasses += "alert-warning";
iconName = "sync";
statusDescription = validStatuses[status];
break;
case validStatuses.FINISHED_SUCCESSFULLY:
pillClasses += "alert-success";
iconName = "check_circle";
statusDescription = validStatuses[status];
break;
case validStatuses.FINISHED_WITH_ERROR:
pillClasses += "alert-danger";
iconName = "error";
statusDescription = validStatuses[status];
break;
default:
pillClasses += "alert-secondary";
iconName = "";
statusDescription = status;
break;
}
return (
<div className={pillClasses} id="job-details-status">
<i className="material-icons text-sm" id="job-details-status-icon">
{iconName}
</i>
&nbsp;
{statusDescription}
</div>
);
}

export function JobDetailsTable({ Component, title, data }): ReactElement {
return (
<div className="row">
<div className="col-12">
<div className="card my-4">
<div className="card-header p-0 position-relative mt-n4 mx-3 z-index-2 col-sm-4">
<div className="bg-gradient-primary shadow-primary border-radius-lg pt-4 pb-3">
<h6 className="text-white text-capitalize text-nowrap ps-3">{title}</h6>
</div>
</div>

<div className="card-body px-0 pb-2">
<div className="table-responsive p-0">
<Component data={data} />
</div>
</div>
</div>
</div>
</div>
);
}

export function JobDetailsServerConfigTable({ data }: { data: string }): ReactElement {
const emptyResponse = (
<div className="container" id="job-details-server-config-empty">
Empty.
</div>
);

if (!data) {
return emptyResponse;
}

const serverConfigJson = JSON.parse(data);

if (typeof serverConfigJson != "object" || Array.isArray(serverConfigJson)) {
return (
<div className="container" id="job-details-server-config-error">
Error parsing server configuration.
</div>
);
}

const serverConfigNames = Object.keys(serverConfigJson);

if (serverConfigNames.length === 0) {
return emptyResponse;
}

return (
<table className="table align-items-center mb-0">
<thead>
<tr>
<th className="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">Name</th>
<th className="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">Value</th>
</tr>
</thead>
<tbody>
{serverConfigNames.map((serverConfigName, i) => (
<tr key={i}>
<td className="col-sm-2" id={`job-details-server-config-name-${i}`}>
<div className="d-flex flex-column justify-content-center">
<span className="ps-3 text-secondary text-sm font-weight-bold">{serverConfigName}</span>
</div>
</td>
<td className="col-sm" id={`job-details-server-config-value-${i}`}>
<div className="d-flex flex-column justify-content-center">
<span className="ps-3 text-secondary text-sm">
{serverConfigJson[serverConfigName]}
</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
);
}

export function JobDetailsClientsInfoTable({ data }: { data: Array<ClientInfo> }): ReactElement {
return (
<table className="table align-items-center mb-0">
<thead>
<tr>
<th className="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">Client</th>
<th className="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">Address</th>
<th className="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">Data Path</th>
<th className="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">Redis Host</th>
<th className="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">Redis Port</th>
</tr>
</thead>
<tbody>
{data.map((clientInfo, i) => (
<tr key={i}>
<td className="col-sm" id={`job-details-client-config-client-${i}`}>
<div className="d-flex flex-column justify-content-center">
<span className="ps-3 text-secondary text-sm">{clientInfo.client}</span>
</div>
</td>
<td className="col-sm" id={`job-details-client-config-service-address-${i}`}>
<div className="d-flex flex-column justify-content-center">
<span className="ps-3 text-secondary text-sm">{clientInfo.service_address}</span>
</div>
</td>
<td className="col-sm" id={`job-details-client-config-data-path-${i}`}>
<div className="d-flex flex-column justify-content-center">
<span className="ps-3 text-secondary text-sm">{clientInfo.data_path}</span>
</div>
</td>
<td className="col-sm" id={`job-details-client-config-redis-host-${i}`}>
<div className="d-flex flex-column justify-content-center">
<span className="ps-3 text-secondary text-sm">{clientInfo.redis_host}</span>
</div>
</td>
<td className="col-sm" id={`job-details-client-config-redis-port-${i}`}>
<div className="d-flex flex-column justify-content-center">
<span className="ps-3 text-secondary text-sm">{clientInfo.redis_port}</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
);
}
Loading

0 comments on commit 992ca66

Please sign in to comment.