Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Job details page, part 1 #71

Merged
merged 23 commits into from
Aug 7, 2024
Merged
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
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} />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small nit: The Status Button is a bit larger than the text so the spacing between the rows of server (and the server config below) are not uniform. Maybe worth increasing size of the text or adding some padding perhaps if that looks slightly more visually pleasing

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean add padding on the other rows so the spacing is the same?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah! Like I said very small nit, if you don't think it really makes a difference feel free to disregard

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, will try :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, please check again.

</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