Skip to content

Commit

Permalink
List jobs page (#21)
Browse files Browse the repository at this point in the history
* First pass at adding page that lists jobs of each status

* Add small updates to jobs sidevar icon and content displayed in job status tables

* Small fix to job model: client_info to clients_info to reflect the actual field name

* Checking if this works

* Adding nextjs-lint to precommit rules

* Adding prettier

* Adding js unit tests

* Adding js unit tests instructions

* Changing prettier indentation to 4 spaces

* Adding autofix and unit tsts on commit hook

* Adding nextjs-unit to the skip list of autofix bot

* Update .pre-commit-config.yaml

* test adding eslintignore

* testing something

* Adding more stuff to ignore

* Reverting the changes to min.js files, adding them to prettier and eslint ignore

* Last change, I promise

* Address CR by Marcelo and add typescript typing

* Add initial unit tests for list jobs page UI

* Add tests to check table contents

* Add better mocking for tests

* Try to fix error with pre-commit on server

* attempt to fix pre-commit ci errors

* Attempt to get prettier to run for pre-commit ci

* remove invalid hook type and add additional dependencies key with yarn

* add prettier dependency

* Change to camel case

* Address CR by Marcelo

* Attempt to resolve pip audit issue with tqdm

---------

Co-authored-by: Marcelo Lotif <[email protected]>
Co-authored-by: Marcelo Lotif <[email protected]>
  • Loading branch information
3 people authored May 3, 2024
1 parent 5252cb3 commit ef82de7
Show file tree
Hide file tree
Showing 11 changed files with 303 additions and 8 deletions.
6 changes: 5 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ repos:
name: prettier-js-format
entry: yarn prettier
files: "florist/app"
language: system
language: node
types: [javascript]
additional_dependencies:
- yarn
- prettier

- repo: local
hooks:
Expand Down
2 changes: 1 addition & 1 deletion florist/api/db/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class Config:
"server_info": '{"n_server_rounds": 3, "batch_size": 8}',
"redis_host": "localhost",
"redis_port": "6879",
"client_info": [
"clients_info": [
{
"client": "MNIST",
"service_address": "locahost:8081",
Expand Down
2 changes: 2 additions & 0 deletions florist/app/client_imports.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ function ClientImports(): null {
return null;
}

const fetcher = (...args) => fetch(...args).then((res) => res.json());
export { fetcher };
export default ClientImports;
10 changes: 10 additions & 0 deletions florist/app/jobs/hooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import useSWR from "swr";
import { fetcher } from "../client_imports";

export default function useGetJobsByJobStatus(status: string) {
const endpoint = "/api/server/job/".concat(status);
const { data, error, isLoading } = useSWR(endpoint, fetcher, {
refresh_interval: 1000,
});
return { data, error, isLoading };
}
136 changes: 136 additions & 0 deletions florist/app/jobs/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"use client";
import { ReactElement } from "react/React";
import useGetJobsByStatus from "./hooks";
import useGetJobsByJobStatus from "./hooks";

export const validStatuses = {
NOT_STARTED: "Not Started",
IN_PROGRESS: "In Progress",
FINISHED_WITH_ERROR: "Finished with Error",
FINISHED_SUCCESSFULLY: "Finished Successfully",
};

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

interface ClientInfo {
client: string;
service_address: string;
data_path: string;
redis_host: string;
redis_port: string;
}

interface StatusProp {
status: string;
}

export default function Page(): ReactElement {
const statusComponents = Object.keys(validStatuses).map((key, i) => (
<Status key={key} status={key} />
));
return (
<div className="mx-4">
<h1> Job Status </h1>
{statusComponents}
</div>
);
}

export function Status({ status }: StatusProp): ReactElement {
const { data, error, isLoading } = useGetJobsByJobStatus(status);
if (error) return <span> Help1</span>;
if (isLoading) return <span> Help2 </span>;

return (
<div>
<h4 data-testid={`status-header-${status}`}>
{validStatuses[status]}
</h4>
<StatusTable data={data} status={status} />
</div>
);
}

export function StatusTable({
data,
status,
}: {
data: Array<JobData>;
status: StatusProp;
}): ReactElement {
if (data.length > 0) {
return (
<table data-testid={`status-table-${status}`} className="table">
<thead>
<tr>
<th style={{ width: "25%" }}>Model</th>
<th style={{ width: "25%" }}>Server Address</th>
<th style={{ width: "50%" }}>
Client Service Addresses{" "}
</th>
</tr>
</thead>
<TableRows data={data} />
</table>
);
} else {
return (
<div>
<span data-testid={`status-no-jobs-${status}`}>
{" "}
No jobs to display.{" "}
</span>
</div>
);
}
}

export function TableRows({ data }: { data: Array<JobData> }): ReactElement {
const tableRows = data.map((d, i) => (
<TableRow
key={i}
model={d.model}
serverAddress={d.server_address}
clientsInfo={d.clients_info}
/>
));

return <tbody>{tableRows}</tbody>;
}

export function TableRow({
model,
serverAddress,
clientsInfo,
}: {
model: string;
serverAddress: string;
clientsInfo: Array<ClientInfo>;
}): ReactElement {
return (
<tr>
<td>{model}</td>
<td>{serverAddress}</td>
<ClientListTableData clientsInfo={clientsInfo} />
</tr>
);
}

export function ClientListTableData({
clientsInfo,
}: {
clientsInfo: Array<ClientInfo>;
}): ReactElement {
const clientServiceAddressesString = clientsInfo
.map((c) => c.service_address)
.join(", ");
return <td> {clientServiceAddressesString} </td>;
}
18 changes: 17 additions & 1 deletion florist/app/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logo_ct from "./assets/img/logo-ct.png";

import Image from "next/image";
import Link from "next/link";
import { ReactElement } from "react/React";

export default function Sidebar(): ReactElement {
Expand Down Expand Up @@ -37,7 +38,7 @@ export default function Sidebar(): ReactElement {
<li className="nav-item">
<a
className="nav-link text-white active bg-gradient-primary"
href="#"
href="/"
>
<div className="text-white text-center me-2 d-flex align-items-center justify-content-center">
<i className="material-icons opacity-10">
Expand All @@ -48,6 +49,21 @@ export default function Sidebar(): ReactElement {
</a>
</li>
</ul>
<ul className="navbar-nav">
<li className="nav-item">
<Link
className="nav-link text-white active bg-gradient-primary"
href="/jobs"
>
<div className="text-white text-center me-2 d-flex align-items-center justify-content-center">
<i className="material-icons opacity-10">
list
</i>
</div>
<span className="nav-link-text ms-1">Jobs</span>
</Link>
</li>
</ul>
</div>
</aside>
);
Expand Down
112 changes: 112 additions & 0 deletions florist/tests/unit/app/jobs/page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import "@testing-library/jest-dom";
import { getByText, render, cleanup } from "@testing-library/react";
import { describe, afterEach, it, expect } from "@jest/globals";

import Page, { validStatuses } from "../../../../app/jobs/page";
import useGetJobsByStatus from "../../../../app/jobs/hooks";

jest.mock("../../../../app/jobs/hooks");

afterEach(() => {
jest.clearAllMocks();
cleanup();
});

function mockJobData(
model: string,
serverAddress: string,
clientServicesAddresses: Array<string>,
) {
const data = {
model: model,
server_address: serverAddress,
clients_info: clientServicesAddresses.map((clientServicesAddress) => ({
service_address: clientServicesAddress,
})),
};
return data;
}

function setupMock(
validStatuses: Array<string>,
data: Array<object>,
error: boolean,
isLoading: boolean,
) {
useGetJobsByStatus.mockImplementation((status: string) => {
if (validStatuses.includes(status)) {
return {
data,
error,
isLoading,
};
} else {
return {
data: [],
error: false,
isLoading: false,
};
}
});
}

describe("List Jobs Page", () => {
it("Renders Page Title correct", () => {
setupMock([], [], false, false);
const { container } = render(<Page />);
const h1 = container.querySelector("h1");
expect(h1).toBeInTheDocument();
expect(h1).toHaveTextContent("Job Status");
});

it("Renders Status Components Headers", () => {
const data = [
mockJobData("MNIST", "localhost:8080", ["localhost:7080"]),
];
const validStatusesKeys = Object.keys(validStatuses);

setupMock(validStatusesKeys, data, false, false);
const { getByTestId } = render(<Page />);

for (const status of validStatusesKeys) {
const element = getByTestId(`status-header-${status}`);
expect(element).toBeInTheDocument();
expect(element).toHaveTextContent(validStatuses[status]);
}
});

it("Renders Status Table With Table with Data", () => {
const data = [
mockJobData("MNIST", "localhost:8080", ["localhost:7080"]),
];
const validStatusesKeys = Object.keys(validStatuses);

setupMock(validStatusesKeys, data, false, false);

const { getByTestId } = render(<Page />);

for (const status of validStatusesKeys) {
const element = getByTestId(`status-table-${status}`);
expect(getByText(element, "Model")).toBeInTheDocument();
expect(getByText(element, "Server Address")).toBeInTheDocument();
expect(
getByText(element, "Client Service Addresses"),
).toBeInTheDocument();
expect(getByText(element, "MNIST")).toBeInTheDocument();
expect(getByText(element, "localhost:8080")).toBeInTheDocument();
expect(getByText(element, "localhost:7080")).toBeInTheDocument();
}
});

it("Renders Status Table With Table without Data", () => {
setupMock([], [], false, false);
const { getByTestId } = render(<Page />);

for (const status of Object.keys(validStatuses)) {
const element = getByTestId(`status-no-jobs-${status}`);
expect(
getByText(element, "No jobs to display."),
).toBeInTheDocument();
}
});
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"prettier": "^3.2.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"swr": "^2.2.5",
"tailwindcss": "3.3.2",
"typescript": "5.0.4",
"yarn": "^1.22.21"
Expand Down
8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ redis = "^5.0.1"
python-multipart = "^0.0.9"
pydantic = "^1.10.15"
motor = "^3.4.0"
tqdm = "^4.66.3"

[tool.poetry.group.test]
optional = true
Expand Down
Loading

0 comments on commit ef82de7

Please sign in to comment.