From 9d7aa20bae678a000138922dc6adf1d29027dc0a Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Wed, 26 Jun 2024 11:44:06 -0400 Subject: [PATCH 01/15] WIP --- florist/app/jobs/details/page.tsx | 11 +++++++++ florist/app/jobs/page.tsx | 39 ++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 florist/app/jobs/details/page.tsx diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx new file mode 100644 index 00000000..49a6231f --- /dev/null +++ b/florist/app/jobs/details/page.tsx @@ -0,0 +1,11 @@ +import { ReactElement } from "react/React"; + +export function JobDetailsHeader(): ReactElement { + return ( +
+
+

Job Details

+
+
+ ); +} diff --git a/florist/app/jobs/page.tsx b/florist/app/jobs/page.tsx index 30d698be..cfcdaf13 100644 --- a/florist/app/jobs/page.tsx +++ b/florist/app/jobs/page.tsx @@ -90,6 +90,28 @@ export function NewJobButton(): ReactElement { ); } +export function JobDetailsButton({ + rowId, + jobId, +}: { + rowId: number; + jobId: string; +}): ReactElement { + return ( +
+ +
+ ); +} + + + export function Status({ status, data }: { status: StatusProp; data: Object }): ReactElement { return (
@@ -126,6 +148,7 @@ export function StatusTable({ data, status }: { data: Array; status: St Client Service Addresses + @@ -146,20 +169,31 @@ export function StatusTable({ data, status }: { data: Array; status: St export function TableRows({ data }: { data: Array }): ReactElement { const tableRows = data.map((d, i) => ( - + )); return {tableRows}; } export function TableRow({ + rowId, model, serverAddress, clientsInfo, + jobId, }: { + rowId: number; model: string; serverAddress: string; clientsInfo: Array; + jobId: string; }): ReactElement { if (clientsInfo === null) { return ; @@ -183,6 +217,9 @@ export function TableRow({
+ + + ); } From 190c75fa18c529ea528728db0f7451c2d4a63963 Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Wed, 3 Jul 2024 11:50:20 -0400 Subject: [PATCH 02/15] Showing job details info --- florist/api/routes/server/job.py | 26 +++- florist/app/jobs/definitions.tsx | 26 ++++ florist/app/jobs/details/page.tsx | 138 +++++++++++++++++++++- florist/app/jobs/hooks.tsx | 6 +- florist/app/jobs/page.tsx | 38 ++---- florist/tests/unit/app/jobs/page.test.tsx | 3 +- 6 files changed, 201 insertions(+), 36 deletions(-) create mode 100644 florist/app/jobs/definitions.tsx diff --git a/florist/api/routes/server/job.py b/florist/api/routes/server/job.py index ab336b81..476a2d71 100644 --- a/florist/api/routes/server/job.py +++ b/florist/api/routes/server/job.py @@ -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 @@ -11,6 +11,26 @@ 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", @@ -36,7 +56,9 @@ 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. diff --git a/florist/app/jobs/definitions.tsx b/florist/app/jobs/definitions.tsx new file mode 100644 index 00000000..5fd5de50 --- /dev/null +++ b/florist/app/jobs/definitions.tsx @@ -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; +} + +export interface ClientInfo { + client: string; + service_address: string; + data_path: string; + redis_host: string; + redis_port: string; +} diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx index 49a6231f..3301a95a 100644 --- a/florist/app/jobs/details/page.tsx +++ b/florist/app/jobs/details/page.tsx @@ -1,11 +1,145 @@ +"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 ( +
+ + +
+ ); +} + export function JobDetailsHeader(): ReactElement { return (
-
-

Job Details

+

Job Details

+
+ ); +} + +export function JobDetailsBody(): ReactElement { + const searchParams = useSearchParams(); + const jobId = searchParams.get("id"); + + const { data: job, error, isLoading } = useGetJob(jobId); + + if (isLoading) { + return ( +
+ Loading +
+ ); + } + if (job) { + return ( +
+
+
+ Job ID: +
+
+ {job._id} +
+
+
+
+ Status: +
+
+ {validStatuses[job.status]} +
+
+
+
+ Model: +
+
+ {job.model} +
+
+
+
+ Server Address: +
+
+ {job.server_address} +
+
+
+
+ Redis Host: +
+
+ {job.redis_host} +
+
+
+
+ Redis Port: +
+
+ {job.redis_port} +
+
+ +
Server Configuration:
+ + +
Clients Configuration:
+ +
+ ); + } + return null; +} + +export function JobDetailsServerConfig({ serverConfig }: { serverConfig: string }): ReactElement { + const serverConfigJson = JSON.parse(serverConfig); + console.log(serverConfigJson) + return ( +
+ {serverConfigJson.map((serverConfigItem, i) => ( +
+
+ {serverConfigItem.name}: +
+
+ {serverConfigItem.value} +
+
+ ))} +
+ ); +} + +export function JobDetailsClientsInfo({ clientsInfo }: { clientsInfo: Array }): ReactElement { + return ( +
+
+
Client
+
Address
+
Data Path
+
Redis Host
+
Redis Port
+ {clientsInfo.map((clientInfo, i) => ( +
+
{clientInfo.client}
+
{clientInfo.service_address}
+
{clientInfo.data_path}
+
{clientInfo.redis_host}
+
{clientInfo.redis_port}
+
+ ))}
); } diff --git a/florist/app/jobs/hooks.tsx b/florist/app/jobs/hooks.tsx index c3e19663..08dd8440 100644 --- a/florist/app/jobs/hooks.tsx +++ b/florist/app/jobs/hooks.tsx @@ -4,13 +4,17 @@ import useSWR, { mutate } from "swr"; import { fetcher } from "../client_imports"; export function useGetJobsByJobStatus(status: string) { - const endpoint = "/api/server/job/".concat(status); + const endpoint = `/api/server/job/status/${status}`; const { data, error, isLoading } = useSWR(endpoint, fetcher, { refresh_interval: 1000, }); return { data, error, isLoading }; } +export function useGetJob(jobId: string) { + return useSWR(`/api/server/job/${jobId}`, fetcher); +} + export function useGetModels() { return useSWR("/api/server/models", fetcher); } diff --git a/florist/app/jobs/page.tsx b/florist/app/jobs/page.tsx index 4792678a..9d4fa295 100644 --- a/florist/app/jobs/page.tsx +++ b/florist/app/jobs/page.tsx @@ -4,39 +4,13 @@ import { useEffect } from "react"; import { ReactElement } from "react/React"; import { refreshJobsByJobStatus, useGetJobsByJobStatus, usePost } from "./hooks"; +import { validStatuses, JobData, ClientInfo } from "./definitions"; import Link from "next/link"; import Image from "next/image"; import loading_gif from "../assets/img/loading.gif"; -// 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", -}; - -interface JobData { - _id: string; - status: string; - model: string; - server_address: string; - server_info: string; - redis_host: string; - redis_port: string; - clients_info: Array; -} - -interface ClientInfo { - client: string; - service_address: string; - data_path: string; - redis_host: string; - redis_port: string; -} - interface StatusProp { status: string; } @@ -147,13 +121,17 @@ export function JobDetailsButton({ }): ReactElement { return (
- +
); } diff --git a/florist/tests/unit/app/jobs/page.test.tsx b/florist/tests/unit/app/jobs/page.test.tsx index 0171ba46..dafced03 100644 --- a/florist/tests/unit/app/jobs/page.test.tsx +++ b/florist/tests/unit/app/jobs/page.test.tsx @@ -2,7 +2,8 @@ import "@testing-library/jest-dom"; import { getByText, render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react"; import { describe, it, expect, afterEach } from "@jest/globals"; -import Page, { validStatuses } from "../../../../app/jobs/page"; +import Page from "../../../../app/jobs/page"; +import { validStatuses } from "../../../../app/jobs/definitions"; import { useGetJobsByJobStatus, usePost } from "../../../../app/jobs/hooks"; import { after } from "node:test"; From c1df0167b8de808031bf18923fce76c7eaf2f18e Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Wed, 3 Jul 2024 13:53:19 -0400 Subject: [PATCH 03/15] Finishing styles --- florist/app/assets/css/florist.css | 8 +++++ florist/app/jobs/details/page.tsx | 50 ++++++++++++++++++++++++++++-- florist/app/jobs/page.tsx | 4 +-- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/florist/app/assets/css/florist.css b/florist/app/assets/css/florist.css index 8fea8042..b294ffca 100644 --- a/florist/app/assets/css/florist.css +++ b/florist/app/assets/css/florist.css @@ -53,3 +53,11 @@ .save-btn { width: 100px; } + +.status-pill { + display: inline-flex; + align-items: center; + padding: 5px 10px; + border-radius: 5px; + color: white; +} diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx index 3301a95a..32c8f75e 100644 --- a/florist/app/jobs/details/page.tsx +++ b/florist/app/jobs/details/page.tsx @@ -51,11 +51,11 @@ export function JobDetailsBody(): ReactElement {
-
+
Status:
- {validStatuses[job.status]} +
@@ -102,9 +102,53 @@ export function JobDetailsBody(): ReactElement { return null; } +export function JobDetailsStatus({ status }: { status: string }): ReactElement { + let pillClasses = "status-pill "; + let iconName; + + console.log(status); + console.log(validStatuses[status]); + + switch (String(validStatuses[status])) { + case validStatuses.NOT_STARTED: + pillClasses += "alert-info"; + iconName = "radio_button_checked"; + break; + case validStatuses.IN_PROGRESS: + pillClasses += "alert-warning"; + iconName = "sync"; + break; + case validStatuses.FINISHED_SUCCESSFULLY: + pillClasses += "alert-success"; + iconName = "check_circle"; + break; + case validStatuses.FINISHED_WITH_ERROR: + pillClasses += "alert-danger"; + iconName = "error"; + break; + default: + pillClasses += "alert-secondary"; + iconName = ""; + break; + } + return ( +
+ {iconName}  + {validStatuses[status]} +
+ ); +} + export function JobDetailsServerConfig({ serverConfig }: { serverConfig: string }): ReactElement { + if (!serverConfig) { + return null; + } + const serverConfigJson = JSON.parse(serverConfig); - console.log(serverConfigJson) + + if (!Array.isArray(serverConfigJson)) { + return null; + } return (
{serverConfigJson.map((serverConfigItem, i) => ( diff --git a/florist/app/jobs/page.tsx b/florist/app/jobs/page.tsx index 9d4fa295..de266f4f 100644 --- a/florist/app/jobs/page.tsx +++ b/florist/app/jobs/page.tsx @@ -174,8 +174,8 @@ export function StatusTable({ data, status }: { data: Array; status: St Client Service Addresses - - + + From bd2f6deed545e376879f4a83798b0f52fa022045 Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Wed, 3 Jul 2024 16:32:09 -0400 Subject: [PATCH 04/15] Testing happy path --- florist/app/jobs/details/page.tsx | 69 ++++++++----- florist/app/jobs/page.tsx | 2 +- .../tests/unit/app/jobs/details/page.test.tsx | 96 +++++++++++++++++++ package.json | 1 + yarn.lock | 5 + 5 files changed, 149 insertions(+), 24 deletions(-) create mode 100644 florist/tests/unit/app/jobs/details/page.test.tsx diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx index 32c8f75e..e2975c87 100644 --- a/florist/app/jobs/details/page.tsx +++ b/florist/app/jobs/details/page.tsx @@ -46,7 +46,7 @@ export function JobDetailsBody(): ReactElement {
Job ID:
-
+
{job._id}
@@ -62,7 +62,7 @@ export function JobDetailsBody(): ReactElement {
Model:
-
+
{job.model}
@@ -70,7 +70,7 @@ export function JobDetailsBody(): ReactElement {
Server Address:
-
+
{job.server_address}
@@ -78,7 +78,7 @@ export function JobDetailsBody(): ReactElement {
Redis Host:
-
+
{job.redis_host}
@@ -86,7 +86,7 @@ export function JobDetailsBody(): ReactElement {
Redis Port:
-
+
{job.redis_port}
@@ -105,10 +105,6 @@ export function JobDetailsBody(): ReactElement { export function JobDetailsStatus({ status }: { status: string }): ReactElement { let pillClasses = "status-pill "; let iconName; - - console.log(status); - console.log(validStatuses[status]); - switch (String(validStatuses[status])) { case validStatuses.NOT_STARTED: pillClasses += "alert-info"; @@ -132,7 +128,7 @@ export function JobDetailsStatus({ status }: { status: string }): ReactElement { break; } return ( -
+
{iconName}  {validStatuses[status]}
@@ -140,24 +136,41 @@ export function JobDetailsStatus({ status }: { status: string }): ReactElement { } export function JobDetailsServerConfig({ serverConfig }: { serverConfig: string }): ReactElement { + const emptyResponse = ( +
+ Empty. +
+ ); + if (!serverConfig) { - return null; + return emptyResponse; } const serverConfigJson = JSON.parse(serverConfig); - if (!Array.isArray(serverConfigJson)) { - return null; + if (typeof serverConfigJson != "object" || Array.isArray(serverConfigJson)) { + return ( +
+ Error parsing server configuration. +
+ ); + } + + const serverConfigNames = Object.keys(serverConfigJson); + + if (serverConfigNames.length === 0) { + return emptyResponse; } + return (
- {serverConfigJson.map((serverConfigItem, i) => ( + {serverConfigNames.map((serverConfigName, i) => (
-
- {serverConfigItem.name}: +
+ {serverConfigName}:
-
- {serverConfigItem.value} +
+ {serverConfigJson[serverConfigName]}
))} @@ -177,11 +190,21 @@ export function JobDetailsClientsInfo({ clientsInfo }: { clientsInfo: Array {clientsInfo.map((clientInfo, i) => (
-
{clientInfo.client}
-
{clientInfo.service_address}
-
{clientInfo.data_path}
-
{clientInfo.redis_host}
-
{clientInfo.redis_port}
+
+ {clientInfo.client} +
+
+ {clientInfo.service_address} +
+
+ {clientInfo.data_path} +
+
+ {clientInfo.redis_host} +
+
+ {clientInfo.redis_port} +
))}
diff --git a/florist/app/jobs/page.tsx b/florist/app/jobs/page.tsx index de266f4f..e7e96b45 100644 --- a/florist/app/jobs/page.tsx +++ b/florist/app/jobs/page.tsx @@ -251,7 +251,7 @@ export function TableRow({ - {validStatuses[status] == "Not Started" ? : null} + {validStatuses[status] === "Not Started" ? : null} ); } diff --git a/florist/tests/unit/app/jobs/details/page.test.tsx b/florist/tests/unit/app/jobs/details/page.test.tsx new file mode 100644 index 00000000..07580e82 --- /dev/null +++ b/florist/tests/unit/app/jobs/details/page.test.tsx @@ -0,0 +1,96 @@ +import "@testing-library/jest-dom"; +import { getByText, render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react"; +import { describe, it, expect, afterEach } from "@jest/globals"; + +import { useGetJob } from "../../../../../app/jobs/hooks"; +import { validStatuses, JobData } from "../../../../../app/jobs/definitions"; +import JobDetails from "../../../../../app/jobs/details/page"; + +const testJobId = "test-job-id"; + +jest.mock("../../../../../app/jobs/hooks"); +jest.mock('next/navigation', () => ({ + ...require("next-router-mock"), + useSearchParams: () => new Map([["id", testJobId]]), +})); + +afterEach(() => { + jest.clearAllMocks(); + cleanup(); +}); + +function setupMocks(jobData: JobData) { + useGetJob.mockImplementation((jobId: string) => { + return { data: jobData, error: null, isLoading: false }; + }); +} + +function makeTestJob(): JobData { + return { + _id: testJobId, + status: "NOT_STARTED", + model: "test-model", + server_address: "test-server-address", + redis_host: "test-redis-host", + redis_port: "test-redis-port", + server_config: JSON.stringify({ + test_attribute_1: "test-value-1", + test_attribute_2: "test-value-2", + }), + clients_info: [ + { + client: "test-client-1", + service_address: "test-service-address-1", + data_path: "test-data-path-1", + redis_host: "test-redis-host-1", + redis_port: "test-redis-port-1", + }, { + client: "test-client-2", + service_address: "test-service-address-2", + data_path: "test-data-path-2", + redis_host: "test-redis-host-2", + redis_port: "test-redis-port-2", + }, + ], + }; +} + +describe("Job Details Page", () => { + it("Renders correctly", () => { + const testJob = makeTestJob(); + setupMocks(testJob); + const { container } = render(); + expect(container.querySelector("h1")).toHaveTextContent("Job Details"); + expect(container.querySelector("#job-details-id")).toHaveTextContent(testJob._id); + expect(container.querySelector("#job-details-status")).toHaveTextContent(validStatuses[testJob.status]); + expect(container.querySelector("#job-details-server-address")).toHaveTextContent(testJob.server_address); + expect(container.querySelector("#job-details-redis-host")).toHaveTextContent(testJob.redis_host); + const testServerConfig = JSON.parse(testJob.server_config); + const serverConfigNames = Object.keys(testServerConfig); + for (let i = 0; i < serverConfigNames.length; i++) { + expect(container.querySelector(`#job-details-server-config-name-${i}`)).toHaveTextContent( + serverConfigNames[i] + ); + expect(container.querySelector(`#job-details-server-config-value-${i}`)).toHaveTextContent( + testServerConfig[serverConfigNames[i]] + ); + } + for (let i = 0; i < testJob.clients_info.length; i++) { + expect(container.querySelector(`#job-details-client-config-client-${i}`)).toHaveTextContent( + testJob.clients_info[i].client + ); + expect(container.querySelector(`#job-details-client-config-service-address-${i}`)).toHaveTextContent( + testJob.clients_info[i].service_address + ); + expect(container.querySelector(`#job-details-client-config-data-path-${i}`)).toHaveTextContent( + testJob.clients_info[i].data_path + ); + expect(container.querySelector(`#job-details-client-config-redis-host-${i}`)).toHaveTextContent( + testJob.clients_info[i].redis_host + ); + expect(container.querySelector(`#job-details-client-config-redis-port-${i}`)).toHaveTextContent( + testJob.clients_info[i].redis_port + ); + } + }); +}); diff --git a/package.json b/package.json index 87cf3448..bc93e864 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@testing-library/react": "^15.0.4", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "next-router-mock": "^0.9.13", "ts-node": "^10.9.2" } } diff --git a/yarn.lock b/yarn.lock index f2c800f4..2e326d34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3629,6 +3629,11 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +next-router-mock@^0.9.13: + version "0.9.13" + resolved "https://registry.yarnpkg.com/next-router-mock/-/next-router-mock-0.9.13.tgz#bdee2011ea6c09e490121c354ef917f339767f72" + integrity sha512-906n2RRaE6Y28PfYJbaz5XZeJ6Tw8Xz1S6E31GGwZ0sXB6/XjldD1/2azn1ZmBmRk5PQRkzjg+n+RHZe5xQzWA== + next@14.1.1: version "14.1.1" resolved "https://registry.yarnpkg.com/next/-/next-14.1.1.tgz#92bd603996c050422a738e90362dff758459a171" From 450e36e56d8970b917cb3f3b71718266e68dbb5b Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Wed, 3 Jul 2024 16:50:05 -0400 Subject: [PATCH 05/15] Adding tests for the statuses --- florist/app/jobs/details/page.tsx | 2 +- .../tests/unit/app/jobs/details/page.test.tsx | 52 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx index e2975c87..9725c1ec 100644 --- a/florist/app/jobs/details/page.tsx +++ b/florist/app/jobs/details/page.tsx @@ -129,7 +129,7 @@ export function JobDetailsStatus({ status }: { status: string }): ReactElement { } return (
- {iconName}  + {iconName}  {validStatuses[status]}
); diff --git a/florist/tests/unit/app/jobs/details/page.test.tsx b/florist/tests/unit/app/jobs/details/page.test.tsx index 07580e82..4ff6e928 100644 --- a/florist/tests/unit/app/jobs/details/page.test.tsx +++ b/florist/tests/unit/app/jobs/details/page.test.tsx @@ -9,7 +9,7 @@ import JobDetails from "../../../../../app/jobs/details/page"; const testJobId = "test-job-id"; jest.mock("../../../../../app/jobs/hooks"); -jest.mock('next/navigation', () => ({ +jest.mock("next/navigation", () => ({ ...require("next-router-mock"), useSearchParams: () => new Map([["id", testJobId]]), })); @@ -60,9 +60,13 @@ describe("Job Details Page", () => { const testJob = makeTestJob(); setupMocks(testJob); const { container } = render(); + + expect(useGetJob).toBeCalledWith(testJobId); + expect(container.querySelector("h1")).toHaveTextContent("Job Details"); expect(container.querySelector("#job-details-id")).toHaveTextContent(testJob._id); expect(container.querySelector("#job-details-status")).toHaveTextContent(validStatuses[testJob.status]); + expect(container.querySelector("#job-details-status")).toHaveClass("status-pill"); expect(container.querySelector("#job-details-server-address")).toHaveTextContent(testJob.server_address); expect(container.querySelector("#job-details-redis-host")).toHaveTextContent(testJob.redis_host); const testServerConfig = JSON.parse(testJob.server_config); @@ -93,4 +97,50 @@ describe("Job Details Page", () => { ); } }); + describe("Status", () => { + it("Renders NOT_STARTED correctly", () => { + const testJob = makeTestJob(); + testJob.status = "NOT_STARTED"; + setupMocks(testJob); + const { container } = render(); + const statusComponent = container.querySelector("#job-details-status") + expect(statusComponent).toHaveTextContent(validStatuses[testJob.status]); + expect(statusComponent).toHaveClass("alert-info"); + const iconComponent = statusComponent.querySelector("#job-details-status-icon"); + expect(iconComponent).toHaveTextContent("radio_button_checked"); + }); + it("Renders IN_PROGRESS correctly", () => { + const testJob = makeTestJob(); + testJob.status = "IN_PROGRESS"; + setupMocks(testJob); + const { container } = render(); + const statusComponent = container.querySelector("#job-details-status") + expect(statusComponent).toHaveTextContent(validStatuses[testJob.status]); + expect(statusComponent).toHaveClass("alert-warning"); + const iconComponent = statusComponent.querySelector("#job-details-status-icon"); + expect(iconComponent).toHaveTextContent("sync"); + }); + it("Renders FINISHED_SUCCESSFULLY correctly", () => { + const testJob = makeTestJob(); + testJob.status = "FINISHED_SUCCESSFULLY"; + setupMocks(testJob); + const { container } = render(); + const statusComponent = container.querySelector("#job-details-status") + expect(statusComponent).toHaveTextContent(validStatuses[testJob.status]); + expect(statusComponent).toHaveClass("alert-success"); + const iconComponent = statusComponent.querySelector("#job-details-status-icon"); + expect(iconComponent).toHaveTextContent("check_circle"); + }); + it("Renders FINISHED_WITH_ERROR correctly", () => { + const testJob = makeTestJob(); + testJob.status = "FINISHED_WITH_ERROR"; + setupMocks(testJob); + const { container } = render(); + const statusComponent = container.querySelector("#job-details-status") + expect(statusComponent).toHaveTextContent(validStatuses[testJob.status]); + expect(statusComponent).toHaveClass("alert-danger"); + const iconComponent = statusComponent.querySelector("#job-details-status-icon"); + expect(iconComponent).toHaveTextContent("error"); + }); + }); }); From 8354886a9b53ad7e19caf54fce0787cba375ecb1 Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Wed, 3 Jul 2024 17:45:56 -0400 Subject: [PATCH 06/15] new page fully tested --- florist/app/jobs/details/page.tsx | 127 ++++++++++-------- .../tests/unit/app/jobs/details/page.test.tsx | 69 ++++++++-- 2 files changed, 133 insertions(+), 63 deletions(-) diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx index 9725c1ec..ef96ce00 100644 --- a/florist/app/jobs/details/page.tsx +++ b/florist/app/jobs/details/page.tsx @@ -35,109 +35,126 @@ export function JobDetailsBody(): ReactElement { if (isLoading) { return (
- Loading + Loading
); } - if (job) { + + if (!job || error) { + if (error) { + console.error(error); + } return (
-
-
- Job ID: -
-
- {job._id} -
+
+ Error retrieving job.
-
-
- Status: -
-
- -
+
+ ); + } + + return ( +
+
+
+ Job ID:
-
-
- Model: -
-
- {job.model} -
+
+ {job._id}
-
-
- Server Address: -
-
- {job.server_address} -
+
+
+
+ Status:
-
-
- Redis Host: -
-
- {job.redis_host} -
+
+
-
-
- Redis Port: -
-
- {job.redis_port} -
+
+
+
+ Model: +
+
+ {job.model} +
+
+
+
+ Server Address: +
+
+ {job.server_address} +
+
+
+
+ Redis Host: +
+
+ {job.redis_host} +
+
+
+
+ Redis Port: +
+
+ {job.redis_port}
+
-
Server Configuration:
- +
Server Configuration:
+ -
Clients Configuration:
- -
- ); - } - return null; +
Clients Configuration:
+ +
+ ); } export function JobDetailsStatus({ status }: { status: string }): ReactElement { let pillClasses = "status-pill "; 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 (
{iconName}  - {validStatuses[status]} + {statusDescription}
); } export function JobDetailsServerConfig({ serverConfig }: { serverConfig: string }): ReactElement { const emptyResponse = ( -
+
Empty.
); @@ -150,7 +167,7 @@ export function JobDetailsServerConfig({ serverConfig }: { serverConfig: string if (typeof serverConfigJson != "object" || Array.isArray(serverConfigJson)) { return ( -
+
Error parsing server configuration.
); diff --git a/florist/tests/unit/app/jobs/details/page.test.tsx b/florist/tests/unit/app/jobs/details/page.test.tsx index 4ff6e928..757fbf45 100644 --- a/florist/tests/unit/app/jobs/details/page.test.tsx +++ b/florist/tests/unit/app/jobs/details/page.test.tsx @@ -1,5 +1,5 @@ import "@testing-library/jest-dom"; -import { getByText, render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react"; +import { render, cleanup } from "@testing-library/react"; import { describe, it, expect, afterEach } from "@jest/globals"; import { useGetJob } from "../../../../../app/jobs/hooks"; @@ -19,9 +19,9 @@ afterEach(() => { cleanup(); }); -function setupMocks(jobData: JobData) { +function setupGetJobMock(data: JobData, isLoading: boolean = false, error = null) { useGetJob.mockImplementation((jobId: string) => { - return { data: jobData, error: null, isLoading: false }; + return { data, error, isLoading }; }); } @@ -58,7 +58,7 @@ function makeTestJob(): JobData { describe("Job Details Page", () => { it("Renders correctly", () => { const testJob = makeTestJob(); - setupMocks(testJob); + setupGetJobMock(testJob); const { container } = render(); expect(useGetJob).toBeCalledWith(testJobId); @@ -101,7 +101,7 @@ describe("Job Details Page", () => { it("Renders NOT_STARTED correctly", () => { const testJob = makeTestJob(); testJob.status = "NOT_STARTED"; - setupMocks(testJob); + setupGetJobMock(testJob); const { container } = render(); const statusComponent = container.querySelector("#job-details-status") expect(statusComponent).toHaveTextContent(validStatuses[testJob.status]); @@ -112,7 +112,7 @@ describe("Job Details Page", () => { it("Renders IN_PROGRESS correctly", () => { const testJob = makeTestJob(); testJob.status = "IN_PROGRESS"; - setupMocks(testJob); + setupGetJobMock(testJob); const { container } = render(); const statusComponent = container.querySelector("#job-details-status") expect(statusComponent).toHaveTextContent(validStatuses[testJob.status]); @@ -123,7 +123,7 @@ describe("Job Details Page", () => { it("Renders FINISHED_SUCCESSFULLY correctly", () => { const testJob = makeTestJob(); testJob.status = "FINISHED_SUCCESSFULLY"; - setupMocks(testJob); + setupGetJobMock(testJob); const { container } = render(); const statusComponent = container.querySelector("#job-details-status") expect(statusComponent).toHaveTextContent(validStatuses[testJob.status]); @@ -134,7 +134,7 @@ describe("Job Details Page", () => { it("Renders FINISHED_WITH_ERROR correctly", () => { const testJob = makeTestJob(); testJob.status = "FINISHED_WITH_ERROR"; - setupMocks(testJob); + setupGetJobMock(testJob); const { container } = render(); const statusComponent = container.querySelector("#job-details-status") expect(statusComponent).toHaveTextContent(validStatuses[testJob.status]); @@ -142,5 +142,58 @@ describe("Job Details Page", () => { const iconComponent = statusComponent.querySelector("#job-details-status-icon"); expect(iconComponent).toHaveTextContent("error"); }); + it("Renders unknown status correctly", () => { + const testJob = makeTestJob(); + testJob.status = "some inexistent status"; + setupGetJobMock(testJob); + const { container } = render(); + const statusComponent = container.querySelector("#job-details-status") + expect(statusComponent).toHaveTextContent(testJob.status); + expect(statusComponent).toHaveClass("alert-secondary"); + const iconComponent = statusComponent.querySelector("#job-details-status-icon"); + expect(iconComponent).toHaveTextContent(""); + }); + }); + describe("Server config", () => { + it("Does not break when it's null", () => { + const testJob = makeTestJob(); + testJob.server_config = null; + setupGetJobMock(testJob); + const { container } = render(); + const serverConfigComponent = container.querySelector("#job-details-server-config-empty"); + expect(serverConfigComponent).toHaveTextContent("Empty."); + }); + it("Does not break when it's an empty dictionary", () => { + const testJob = makeTestJob(); + testJob.server_config = JSON.stringify({}); + setupGetJobMock(testJob); + const { container } = render(); + const serverConfigComponent = container.querySelector("#job-details-server-config-empty"); + expect(serverConfigComponent).toHaveTextContent("Empty."); + }); + it("Does not break when it's not a dictionary", () => { + const testJob = makeTestJob(); + testJob.server_config = JSON.stringify(["bad server config"]); + setupGetJobMock(testJob); + const { container } = render(); + const serverConfigComponent = container.querySelector("#job-details-server-config-error"); + expect(serverConfigComponent).toHaveTextContent("Error parsing server configuration."); + }); + }); + it("Renders loading gif correctly", () => { + setupGetJobMock(null, true); + const { container } = render(); + const loadingComponent = container.querySelector("img#job-details-loading"); + expect(loadingComponent.getAttribute("alt")).toBe("Loading"); + }); + it("Renders error message when job is null", () => { + setupGetJobMock(null); + const { container } = render(); + expect(container.querySelector("#job-details-error")).toHaveTextContent("Error retrieving job."); + }); + it("Renders error message when there is an error", () => { + setupGetJobMock({}, false, "error"); + const { container } = render(); + expect(container.querySelector("#job-details-error")).toHaveTextContent("Error retrieving job."); }); }); From 3e2fdc6f6dcb6490751ab44a9499a9153d135844 Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Wed, 3 Jul 2024 18:09:10 -0400 Subject: [PATCH 07/15] Added test for the button as well --- florist/app/jobs/page.tsx | 6 ++++-- florist/tests/unit/app/jobs/page.test.tsx | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/florist/app/jobs/page.tsx b/florist/app/jobs/page.tsx index e7e96b45..444892bf 100644 --- a/florist/app/jobs/page.tsx +++ b/florist/app/jobs/page.tsx @@ -115,14 +115,16 @@ export function StartJobButton({ rowId, jobId }: { rowId: number; jobId: string export function JobDetailsButton({ rowId, jobId, + status, }: { rowId: number; jobId: string; + status: string; }): ReactElement { return (
- + {validStatuses[status] === "Not Started" ? : null} diff --git a/florist/tests/unit/app/jobs/page.test.tsx b/florist/tests/unit/app/jobs/page.test.tsx index dafced03..a3e88226 100644 --- a/florist/tests/unit/app/jobs/page.test.tsx +++ b/florist/tests/unit/app/jobs/page.test.tsx @@ -16,6 +16,7 @@ afterEach(() => { function mockJobData(model: string, serverAddress: string, clientServicesAddresses: Array) { const data = { + _id: "test-id", model: model, server_address: serverAddress, clients_info: clientServicesAddresses.map((clientServicesAddress) => ({ @@ -150,6 +151,7 @@ describe("List Jobs Page", () => { expect(getByText(element, "No jobs to display.")).toBeInTheDocument(); } }); + it("Renders Loading GIF only when all isLoading", () => { setupMock(["NOT_STARTED", "IN_PROGRESS", "FINISHED_SUCCESSFULLY", "FINISHED_WITH_ERROR"], [], false, true); const { getByTestId } = render(); @@ -164,6 +166,20 @@ describe("List Jobs Page", () => { expect(element).not.toBeInTheDocument(); }); + it("Details button is present on all statuses", () => { + const data = mockJobData("MNIST", "localhost:8080", ["localhost:7080"]); + const validStatusesKeys = Object.keys(validStatuses); + + setupMock(validStatusesKeys, [data], false, false); + const { queryByTestId } = render(); + + for (let status of validStatusesKeys) { + const element = queryByTestId(`job-details-button-${status}-0`); + expect(element.getAttribute("alt")).toBe("Details"); + expect(element.getAttribute("href")).toBe(`jobs/details?id=${data._id}`); + } + }); + it("Start training button present in NOT_STARTED jobs", () => { const data = [mockJobData("MNIST", "localhost:8080", ["localhost:7080"])]; const validStatusesKeys = Object.keys(validStatuses); From 2dd0dec657efb19efa0beb4b7c869248f3063868 Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Wed, 3 Jul 2024 18:17:41 -0400 Subject: [PATCH 08/15] Python unit test --- .../tests/unit/api/routes/server/test_job.py | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/florist/tests/unit/api/routes/server/test_job.py b/florist/tests/unit/api/routes/server/test_job.py index 3e448b56..d4d7d0ef 100644 --- a/florist/tests/unit/api/routes/server/test_job.py +++ b/florist/tests/unit/api/routes/server/test_job.py @@ -3,9 +3,45 @@ from unittest.mock import patch, Mock, AsyncMock from fastapi.responses import JSONResponse -from florist.api.routes.server.job import change_job_status +from florist.api.routes.server.job import change_job_status, get_job from florist.api.db.entities import JobStatus + +@patch("florist.api.db.entities.Job.find_by_id") +async def test_get_job_success(mock_find_by_id: Mock) -> None: + mock_job = Mock() + mock_find_by_id.return_value = mock_job + + mock_request = Mock() + mock_request.app.database = Mock() + + test_id = "test_id" + + response = await get_job(test_id, mock_request) + + mock_find_by_id.assert_called_once_with(test_id, mock_request.app.database) + + assert response == mock_job + + +@patch("florist.api.db.entities.Job.find_by_id") +async def test_get_job_fail_none_job(mock_find_by_id: Mock) -> None: + mock_find_by_id.return_value = None + + mock_request = Mock() + mock_request.app.database = Mock() + + test_id = "test_id" + + response = await get_job(test_id, mock_request) + + mock_find_by_id.assert_called_once_with(test_id, mock_request.app.database) + + assert isinstance(response, JSONResponse) + assert response.status_code == 400 + assert json.loads(response.body.decode("utf-8")) == {"error": f"Job with ID {test_id} does not exist."} + + @patch("florist.api.db.entities.Job.find_by_id") async def test_change_job_status_success(mock_find_by_id: Mock) -> None: mock_job = Mock() @@ -28,6 +64,7 @@ async def test_change_job_status_success(mock_find_by_id: Mock) -> None: assert response.status_code == 200 assert json.loads(response.body.decode("utf-8")) == {"status": "success"} + @patch("florist.api.db.entities.Job.find_by_id") async def test_change_job_status_failure_in_find_by_id(mock_find_by_id: Mock) -> None: mock_find_by_id.return_value = None From 04228aed551be4ef4ac6bb4e77e3d812620619a8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 22:21:28 +0000 Subject: [PATCH 09/15] [pre-commit.ci] Add auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- florist/app/jobs/details/page.tsx | 29 ++++++++++++++----- florist/app/jobs/page.tsx | 4 +-- .../tests/unit/app/jobs/details/page.test.tsx | 27 ++++++++--------- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx index ef96ce00..17e12ca1 100644 --- a/florist/app/jobs/details/page.tsx +++ b/florist/app/jobs/details/page.tsx @@ -1,6 +1,6 @@ -"use client" +"use client"; -import { useSearchParams } from "next/navigation" +import { useSearchParams } from "next/navigation"; import Image from "next/image"; import { ReactElement } from "react/React"; @@ -146,7 +146,10 @@ export function JobDetailsStatus({ status }: { status: string }): ReactElement { } return (
- {iconName}  + + {iconName} + +   {statusDescription}
); @@ -199,11 +202,21 @@ export function JobDetailsClientsInfo({ clientsInfo }: { clientsInfo: Array
-
Client
-
Address
-
Data Path
-
Redis Host
-
Redis Port
+
+ Client +
+
+ Address +
+
+ Data Path +
+
+ Redis Host +
+
+ Redis Port +
{clientsInfo.map((clientInfo, i) => (
diff --git a/florist/app/jobs/page.tsx b/florist/app/jobs/page.tsx index 444892bf..215967b2 100644 --- a/florist/app/jobs/page.tsx +++ b/florist/app/jobs/page.tsx @@ -129,7 +129,7 @@ export function JobDetailsButton({ alt="Details" href={{ pathname: "jobs/details", - query: { "id": jobId }, + query: { id: jobId }, }} > settings @@ -138,8 +138,6 @@ export function JobDetailsButton({ ); } - - export function Status({ status, data }: { status: StatusProp; data: Object }): ReactElement { return (
diff --git a/florist/tests/unit/app/jobs/details/page.test.tsx b/florist/tests/unit/app/jobs/details/page.test.tsx index 757fbf45..cd55068f 100644 --- a/florist/tests/unit/app/jobs/details/page.test.tsx +++ b/florist/tests/unit/app/jobs/details/page.test.tsx @@ -44,7 +44,8 @@ function makeTestJob(): JobData { data_path: "test-data-path-1", redis_host: "test-redis-host-1", redis_port: "test-redis-port-1", - }, { + }, + { client: "test-client-2", service_address: "test-service-address-2", data_path: "test-data-path-2", @@ -73,27 +74,27 @@ describe("Job Details Page", () => { const serverConfigNames = Object.keys(testServerConfig); for (let i = 0; i < serverConfigNames.length; i++) { expect(container.querySelector(`#job-details-server-config-name-${i}`)).toHaveTextContent( - serverConfigNames[i] + serverConfigNames[i], ); expect(container.querySelector(`#job-details-server-config-value-${i}`)).toHaveTextContent( - testServerConfig[serverConfigNames[i]] + testServerConfig[serverConfigNames[i]], ); } for (let i = 0; i < testJob.clients_info.length; i++) { expect(container.querySelector(`#job-details-client-config-client-${i}`)).toHaveTextContent( - testJob.clients_info[i].client + testJob.clients_info[i].client, ); expect(container.querySelector(`#job-details-client-config-service-address-${i}`)).toHaveTextContent( - testJob.clients_info[i].service_address + testJob.clients_info[i].service_address, ); expect(container.querySelector(`#job-details-client-config-data-path-${i}`)).toHaveTextContent( - testJob.clients_info[i].data_path + testJob.clients_info[i].data_path, ); expect(container.querySelector(`#job-details-client-config-redis-host-${i}`)).toHaveTextContent( - testJob.clients_info[i].redis_host + testJob.clients_info[i].redis_host, ); expect(container.querySelector(`#job-details-client-config-redis-port-${i}`)).toHaveTextContent( - testJob.clients_info[i].redis_port + testJob.clients_info[i].redis_port, ); } }); @@ -103,7 +104,7 @@ describe("Job Details Page", () => { testJob.status = "NOT_STARTED"; setupGetJobMock(testJob); const { container } = render(); - const statusComponent = container.querySelector("#job-details-status") + const statusComponent = container.querySelector("#job-details-status"); expect(statusComponent).toHaveTextContent(validStatuses[testJob.status]); expect(statusComponent).toHaveClass("alert-info"); const iconComponent = statusComponent.querySelector("#job-details-status-icon"); @@ -114,7 +115,7 @@ describe("Job Details Page", () => { testJob.status = "IN_PROGRESS"; setupGetJobMock(testJob); const { container } = render(); - const statusComponent = container.querySelector("#job-details-status") + const statusComponent = container.querySelector("#job-details-status"); expect(statusComponent).toHaveTextContent(validStatuses[testJob.status]); expect(statusComponent).toHaveClass("alert-warning"); const iconComponent = statusComponent.querySelector("#job-details-status-icon"); @@ -125,7 +126,7 @@ describe("Job Details Page", () => { testJob.status = "FINISHED_SUCCESSFULLY"; setupGetJobMock(testJob); const { container } = render(); - const statusComponent = container.querySelector("#job-details-status") + const statusComponent = container.querySelector("#job-details-status"); expect(statusComponent).toHaveTextContent(validStatuses[testJob.status]); expect(statusComponent).toHaveClass("alert-success"); const iconComponent = statusComponent.querySelector("#job-details-status-icon"); @@ -136,7 +137,7 @@ describe("Job Details Page", () => { testJob.status = "FINISHED_WITH_ERROR"; setupGetJobMock(testJob); const { container } = render(); - const statusComponent = container.querySelector("#job-details-status") + const statusComponent = container.querySelector("#job-details-status"); expect(statusComponent).toHaveTextContent(validStatuses[testJob.status]); expect(statusComponent).toHaveClass("alert-danger"); const iconComponent = statusComponent.querySelector("#job-details-status-icon"); @@ -147,7 +148,7 @@ describe("Job Details Page", () => { testJob.status = "some inexistent status"; setupGetJobMock(testJob); const { container } = render(); - const statusComponent = container.querySelector("#job-details-status") + const statusComponent = container.querySelector("#job-details-status"); expect(statusComponent).toHaveTextContent(testJob.status); expect(statusComponent).toHaveClass("alert-secondary"); const iconComponent = statusComponent.querySelector("#job-details-status-icon"); From 272d722dcd41592a0b381c9fda25cd4d7751fec5 Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Fri, 5 Jul 2024 13:38:42 -0400 Subject: [PATCH 10/15] Fixing refreshJobsByJobStatus function --- florist/app/jobs/hooks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/florist/app/jobs/hooks.tsx b/florist/app/jobs/hooks.tsx index 08dd8440..15bd5dec 100644 --- a/florist/app/jobs/hooks.tsx +++ b/florist/app/jobs/hooks.tsx @@ -53,5 +53,5 @@ export const usePost = () => { }; export function refreshJobsByJobStatus(statuses: Array) { - statuses.forEach((status: string) => mutate(`/api/server/job/${status}`)); + statuses.forEach((status: string) => mutate(`/api/server/job/status/${status}`)); } From 0932b48d5fa850d12d2eaa9a7dcf4de30bd37fa1 Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Fri, 5 Jul 2024 13:42:07 -0400 Subject: [PATCH 11/15] Small code cleanup --- florist/api/routes/server/job.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/florist/api/routes/server/job.py b/florist/api/routes/server/job.py index 476a2d71..be9dcdad 100644 --- a/florist/api/routes/server/job.py +++ b/florist/api/routes/server/job.py @@ -12,7 +12,10 @@ @router.get( - path="/{job_id}", response_description="Retrieves a job by ID", status_code=status.HTTP_200_OK, response_model=Job + 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]: """ @@ -57,7 +60,9 @@ async def new_job(request: Request, job: Job = Body(...)) -> Job: # noqa: B008 @router.get( - path="/status/{status}", response_description="List jobs with the specified status", response_model=List[Job] + 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]: """ From e6f79bf9228e0f434095fda64ff3e8bbf55cf6b2 Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Mon, 15 Jul 2024 15:07:19 -0400 Subject: [PATCH 12/15] CR by John --- florist/app/jobs/details/page.tsx | 206 ++++++++++++++++++++---------- 1 file changed, 140 insertions(+), 66 deletions(-) diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx index 17e12ca1..fea99920 100644 --- a/florist/app/jobs/details/page.tsx +++ b/florist/app/jobs/details/page.tsx @@ -55,7 +55,7 @@ export function JobDetailsBody(): ReactElement { return (
-
+
Job ID:
@@ -63,7 +63,7 @@ export function JobDetailsBody(): ReactElement { {job._id}
-
+
Status:
@@ -71,7 +71,7 @@ export function JobDetailsBody(): ReactElement {
-
+
Model:
@@ -79,7 +79,7 @@ export function JobDetailsBody(): ReactElement { {job.model}
-
+
Server Address:
@@ -87,7 +87,7 @@ export function JobDetailsBody(): ReactElement { {job.server_address}
-
+
Redis Host:
@@ -95,7 +95,7 @@ export function JobDetailsBody(): ReactElement { {job.redis_host}
-
+
Redis Port:
@@ -104,11 +104,18 @@ export function JobDetailsBody(): ReactElement {
-
Server Configuration:
- + + + -
Clients Configuration:
-
); } @@ -155,22 +162,45 @@ export function JobDetailsStatus({ status }: { status: string }): ReactElement { ); } -export function JobDetailsServerConfig({ serverConfig }: { serverConfig: string }): ReactElement { +export function JobDetailsTableHeader({ Component, title, data }): ReactElement { + return ( +
+
+
+
+
+
{title}
+
+
+ +
+
+ +
+
+ +
+
+
+ ) +} + +export function JobDetailsServerConfigTable({ data }: { data: string }): ReactElement { const emptyResponse = ( -
+
Empty.
); - if (!serverConfig) { + if (!data) { return emptyResponse; } - const serverConfigJson = JSON.parse(serverConfig); + const serverConfigJson = JSON.parse(data); if (typeof serverConfigJson != "object" || Array.isArray(serverConfigJson)) { return ( -
+
Error parsing server configuration.
); @@ -183,60 +213,104 @@ export function JobDetailsServerConfig({ serverConfig }: { serverConfig: string } return ( -
- {serverConfigNames.map((serverConfigName, i) => ( -
-
- {serverConfigName}: -
-
- {serverConfigJson[serverConfigName]} -
-
- ))} -
+ + + + + + + + + {serverConfigNames.map((serverConfigName, i) => ( + + + + + ))} + +
+ Name + + Value +
+
+ + {serverConfigName} + +
+
+
+ + {serverConfigJson[serverConfigName]} + +
+
); } -export function JobDetailsClientsInfo({ clientsInfo }: { clientsInfo: Array }): ReactElement { +export function JobDetailsClientsInfoTable({ data }: { data: Array }): ReactElement { return ( -
-
-
- Client -
-
- Address -
-
- Data Path -
-
- Redis Host -
-
- Redis Port -
-
- {clientsInfo.map((clientInfo, i) => ( -
-
- {clientInfo.client} -
-
- {clientInfo.service_address} -
-
- {clientInfo.data_path} -
-
- {clientInfo.redis_host} -
-
- {clientInfo.redis_port} -
-
- ))} -
+ + + + + + + + + + + + {data.map((clientInfo, i) => ( + + + + + + + + ))} + +
+ Client + + Address + + Data Path + + Redis Host + + Redis Port +
+
+ + {clientInfo.client} + +
+
+
+ + {clientInfo.service_address} + +
+
+
+ + {clientInfo.data_path} + +
+
+
+ + {clientInfo.redis_host} + +
+
+
+ + {clientInfo.redis_port} + +
+
); } From 1f0944ec3998295b86d1da91494fe675051e5811 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 19:07:44 +0000 Subject: [PATCH 13/15] [pre-commit.ci] Add auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- florist/app/jobs/details/page.tsx | 68 +++++++++---------------------- 1 file changed, 20 insertions(+), 48 deletions(-) diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx index fea99920..d533e4e4 100644 --- a/florist/app/jobs/details/page.tsx +++ b/florist/app/jobs/details/page.tsx @@ -115,7 +115,6 @@ export function JobDetailsBody(): ReactElement { title="Clients Configuration" data={job.clients_info} /> -
); } @@ -178,11 +177,10 @@ export function JobDetailsTableHeader({ Component, title, data }): ReactElement
-
- ) + ); } export function JobDetailsServerConfigTable({ data }: { data: string }): ReactElement { @@ -216,12 +214,8 @@ export function JobDetailsServerConfigTable({ data }: { data: string }): ReactEl - - + + @@ -229,12 +223,10 @@ export function JobDetailsServerConfigTable({ data }: { data: string }): ReactEl -
- Name - - Value - NameValue
- - {serverConfigName} - + {serverConfigName}
+
{serverConfigJson[serverConfigName]} @@ -253,59 +245,39 @@ export function JobDetailsClientsInfoTable({ data }: { data: Array } - - - - - + + + + + {data.map((clientInfo, i) => ( - - - - - From d9d225b2c29421e583d16fd2a1712cface98f856 Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Mon, 15 Jul 2024 17:05:11 -0400 Subject: [PATCH 14/15] Small text on status pill --- florist/app/jobs/details/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx index d533e4e4..7e298df8 100644 --- a/florist/app/jobs/details/page.tsx +++ b/florist/app/jobs/details/page.tsx @@ -120,7 +120,7 @@ export function JobDetailsBody(): ReactElement { } export function JobDetailsStatus({ status }: { status: string }): ReactElement { - let pillClasses = "status-pill "; + let pillClasses = "status-pill text-sm "; let iconName; let statusDescription; switch (String(validStatuses[status])) { From c6a757368575c290b0049c606cbc2a2b0f4136ab Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Mon, 15 Jul 2024 17:10:50 -0400 Subject: [PATCH 15/15] Changing name of the component once again --- florist/app/jobs/details/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx index 7e298df8..f632143e 100644 --- a/florist/app/jobs/details/page.tsx +++ b/florist/app/jobs/details/page.tsx @@ -104,13 +104,13 @@ export function JobDetailsBody(): ReactElement { - -
- Client - - Address - - Data Path - - Redis Host - - Redis Port - ClientAddressData PathRedis HostRedis Port
+
- - {clientInfo.client} - + {clientInfo.client}
+
- - {clientInfo.service_address} - + {clientInfo.service_address}
+
- - {clientInfo.data_path} - + {clientInfo.data_path}
+
- - {clientInfo.redis_host} - + {clientInfo.redis_host}
+
- - {clientInfo.redis_port} - + {clientInfo.redis_port}