Merge pull request #18 from jaypyles/16-create-a-favorites-table

feat: add favorites tab in jobs
This commit is contained in:
Jayden Pyles
2024-07-21 21:31:41 -05:00
committed by GitHub
15 changed files with 355 additions and 140 deletions

View File

@@ -17,6 +17,7 @@ From the table, users can download an excel sheet of the job's results, along wi
- Download csv containing results
- Rerun jobs
- View status of queued jobs
- Favorite and view favorited jobs
![job_page](https://github.com/jaypyles/www-scrape/blob/master/docs/job_page.png)

View File

@@ -24,6 +24,7 @@ from api.backend.job import (
query,
insert,
delete_jobs,
update_job,
)
from api.backend.models import (
DownloadJob,
@@ -31,6 +32,7 @@ from api.backend.models import (
SubmitScrapeJob,
DeleteScrapeJobs,
RetrieveScrapeJobs,
UpdateJobs,
)
from api.backend.auth.auth_router import auth_router
import traceback
@@ -67,6 +69,12 @@ def read_favicon():
return FileResponse("dist/favicon.ico")
@app.post("/api/update")
async def update(update_jobs: UpdateJobs):
"""Used to update jobs"""
await update_job(update_jobs.ids, update_jobs.field, update_jobs.value)
@app.post("/api/submit-scrape-job")
async def submit_scrape_job(job: SubmitScrapeJob, background_tasks: BackgroundTasks):
LOG.info(f"Recieved job: {job}")

View File

@@ -34,13 +34,13 @@ async def query(filter: dict[str, Any]) -> list[dict[str, Any]]:
return results
async def update_job(id: str, field: str, value: Any):
async def update_job(ids: list[str], field: str, value: Any):
collection = get_job_collection()
result = await collection.update_one(
{"id": id},
{"$set": {field: value}},
)
return result.modified_count
for id in ids:
_ = await collection.update_one(
{"id": id},
{"$set": {field: value}},
)
async def delete_jobs(jobs: list[str]):

View File

@@ -48,3 +48,9 @@ class DeleteScrapeJobs(pydantic.BaseModel):
class GetStatistics(pydantic.BaseModel):
user: str
class UpdateJobs(pydantic.BaseModel):
ids: list[str]
field: str
value: Any

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -1,19 +1,9 @@
import React, { useState } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableRow,
IconButton,
Box,
Typography,
Accordion,
AccordionSummary,
AccordionDetails,
Checkbox,
Tooltip,
Button,
TextField,
FormControl,
InputLabel,
@@ -23,19 +13,11 @@ import {
} from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import SelectAllIcon from "@mui/icons-material/SelectAll";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import DownloadIcon from "@mui/icons-material/Download";
import StarIcon from "@mui/icons-material/Star";
import { useRouter } from "next/router";
interface Job {
id: string;
url: string;
elements: Object[];
result: Object;
time_created: Date;
status: string;
job_options: Object;
}
import { Favorites, JobQueue } from "./jobs";
import { Job } from "../types";
interface JobTableProps {
jobs: Job[];
@@ -57,6 +39,7 @@ const JobTable: React.FC<JobTableProps> = ({ jobs, fetchJobs }) => {
const [allSelected, setAllSelected] = useState(false);
const [searchQuery, setSearchQuery] = useState<string>("");
const [searchMode, setSearchMode] = useState<string>("url");
const [favoriteView, setFavoriteView] = useState<boolean>(false);
const router = useRouter();
@@ -139,6 +122,22 @@ const JobTable: React.FC<JobTableProps> = ({ jobs, fetchJobs }) => {
return true;
});
const favoriteJob = async (ids: string[], field: string, value: any) => {
const postBody = {
ids: ids,
field: field,
value: value,
};
await fetch("/api/update", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(postBody),
});
await fetchJobs();
};
return (
<Box
width="100%"
@@ -191,6 +190,16 @@ const JobTable: React.FC<JobTableProps> = ({ jobs, fetchJobs }) => {
</IconButton>
</span>
</Tooltip>
<Tooltip title="Favorites">
<span>
<IconButton
color={favoriteView ? "warning" : "default"}
onClick={() => setFavoriteView(!favoriteView)}
>
<StarIcon />
</IconButton>
</span>
</Tooltip>
</div>
<div className="flex flex-row space-x-2 w-1/2">
<TextField
@@ -219,117 +228,23 @@ const JobTable: React.FC<JobTableProps> = ({ jobs, fetchJobs }) => {
</div>
</Box>
<Box sx={{ overflow: "auto" }}>
<Table sx={{ tableLayout: "fixed", width: "100%" }}>
<TableHead>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Id</TableCell>
<TableCell>Url</TableCell>
<TableCell>Elements</TableCell>
<TableCell>Result</TableCell>
<TableCell>Time Created</TableCell>
<TableCell>Status</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredJobs.map((row, index) => (
<TableRow key={index}>
<TableCell padding="checkbox">
<Checkbox
checked={selectedJobs.has(row.id)}
onChange={() => handleSelectJob(row.id)}
/>
</TableCell>
<TableCell sx={{ maxWidth: 100, overflow: "auto" }}>
<Box sx={{ maxHeight: 100, overflow: "auto" }}>
{row.id}
</Box>
</TableCell>
<TableCell sx={{ maxWidth: 200, overflow: "auto" }}>
<Box sx={{ maxHeight: 100, overflow: "auto" }}>
{row.url}
</Box>
</TableCell>
<TableCell sx={{ maxWidth: 150, overflow: "auto" }}>
<Box sx={{ maxHeight: 100, overflow: "auto" }}>
{JSON.stringify(row.elements)}
</Box>
</TableCell>
<TableCell
sx={{ maxWidth: 150, overflow: "auto", padding: 0 }}
>
<Accordion sx={{ margin: 0, padding: 0.5 }}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1a-content"
id="panel1a-header"
sx={{
minHeight: 0,
"&.Mui-expanded": { minHeight: 0 },
}}
>
<Box
sx={{
maxHeight: 150,
overflow: "auto",
width: "100%",
}}
>
<Typography sx={{ fontSize: "0.875rem" }}>
Show Result
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails sx={{ padding: 1 }}>
<Box sx={{ maxHeight: 200, overflow: "auto" }}>
<Typography
sx={{
fontSize: "0.875rem",
whiteSpace: "pre-wrap",
}}
>
{JSON.stringify(row.result, null, 2)}
</Typography>
</Box>
</AccordionDetails>
</Accordion>
</TableCell>
<TableCell sx={{ maxWidth: 150, overflow: "auto" }}>
<Box sx={{ maxHeight: 100, overflow: "auto" }}>
{new Date(row.time_created).toLocaleString()}
</Box>
</TableCell>
<TableCell sx={{ maxWidth: 150, overflow: "auto" }}>
<Box sx={{ maxHeight: 100, overflow: "auto" }}>
<Box
className="rounded-md p-2 text-center"
sx={{ bgcolor: COLOR_MAP[row.status], opactity: "50%" }}
>
{row.status}
</Box>
</Box>
</TableCell>
<TableCell sx={{ maxWidth: 100, overflow: "auto" }}>
<Button
onClick={() => {
handleDownload([row.id]);
}}
>
Download
</Button>
<Button
onClick={() =>
handleNavigate(row.elements, row.url, row.job_options)
}
>
Rerun
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{!favoriteView ? (
<JobQueue
stateProps={{ selectedJobs, filteredJobs }}
colors={COLOR_MAP}
onDownload={handleDownload}
onNavigate={handleNavigate}
onSelectJob={handleSelectJob}
onFavorite={favoriteJob}
></JobQueue>
) : (
<Favorites
stateProps={{ selectedJobs, filteredJobs }}
onNavigate={handleNavigate}
onSelectJob={handleSelectJob}
onFavorite={favoriteJob}
></Favorites>
)}
</Box>
</Box>
</Box>

View File

@@ -0,0 +1,102 @@
import React from "react";
import {
Tooltip,
IconButton,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Box,
Checkbox,
Button,
} from "@mui/material";
import { Job } from "../../types";
import StarIcon from "@mui/icons-material/Star";
interface stateProps {
selectedJobs: Set<string>;
filteredJobs: Job[];
}
interface Props {
onSelectJob: (job: string) => void;
onNavigate: (elements: Object[], url: string, options: any) => void;
onFavorite: (ids: string[], field: string, value: any) => void;
stateProps: stateProps;
}
export const Favorites = ({
stateProps,
onSelectJob,
onNavigate,
onFavorite,
}: Props) => {
const { selectedJobs, filteredJobs } = stateProps;
const favoritedJobs = filteredJobs.filter((job) => job.favorite);
return (
<Table sx={{ tableLayout: "fixed", width: "100%" }}>
<TableHead>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Id</TableCell>
<TableCell>Url</TableCell>
<TableCell>Elements</TableCell>
<TableCell>Time Created</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{favoritedJobs.map((row, index) => (
<TableRow key={index}>
<TableCell padding="checkbox">
<Checkbox
checked={selectedJobs.has(row.id)}
onChange={() => onSelectJob(row.id)}
/>
<Tooltip title="Favorite Job">
<span>
<IconButton
color={row.favorite ? "warning" : "default"}
onClick={() => {
onFavorite([row.id], "favorite", !row.favorite);
row.favorite = !row.favorite;
}}
>
<StarIcon />
</IconButton>
</span>
</Tooltip>
</TableCell>
<TableCell sx={{ maxWidth: 100, overflow: "auto" }}>
<Box sx={{ maxHeight: 100, overflow: "auto" }}>{row.id}</Box>
</TableCell>
<TableCell sx={{ maxWidth: 200, overflow: "auto" }}>
<Box sx={{ maxHeight: 100, overflow: "auto" }}>{row.url}</Box>
</TableCell>
<TableCell sx={{ maxWidth: 150, overflow: "auto" }}>
<Box sx={{ maxHeight: 100, overflow: "auto" }}>
{JSON.stringify(row.elements)}
</Box>
</TableCell>
<TableCell sx={{ maxWidth: 150, overflow: "auto" }}>
<Box sx={{ maxHeight: 100, overflow: "auto" }}>
{new Date(row.time_created).toLocaleString()}
</Box>
</TableCell>
<TableCell sx={{ maxWidth: 100, overflow: "auto" }}>
<Button
onClick={() =>
onNavigate(row.elements, row.url, row.job_options)
}
>
Run
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
};

View File

@@ -0,0 +1,170 @@
import React from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Box,
Typography,
Accordion,
AccordionSummary,
AccordionDetails,
Checkbox,
Button,
Tooltip,
IconButton,
} from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import StarIcon from "@mui/icons-material/Star";
import { Job } from "../../types";
interface stringMap {
[key: string]: string;
}
interface stateProps {
selectedJobs: Set<string>;
filteredJobs: Job[];
}
interface Props {
colors: stringMap;
onSelectJob: (job: string) => void;
onDownload: (job: string[]) => void;
onNavigate: (elements: Object[], url: string, options: any) => void;
onFavorite: (ids: string[], field: string, value: any) => void;
stateProps: stateProps;
}
export const JobQueue = ({
stateProps,
colors,
onSelectJob,
onDownload,
onNavigate,
onFavorite,
}: Props) => {
const { selectedJobs, filteredJobs } = stateProps;
return (
<Table sx={{ tableLayout: "fixed", width: "100%" }}>
<TableHead>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Id</TableCell>
<TableCell>Url</TableCell>
<TableCell>Elements</TableCell>
<TableCell>Result</TableCell>
<TableCell>Time Created</TableCell>
<TableCell>Status</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredJobs.map((row, index) => (
<TableRow key={index}>
<TableCell padding="checkbox">
<Checkbox
checked={selectedJobs.has(row.id)}
onChange={() => onSelectJob(row.id)}
/>
<Tooltip title="Favorite Job">
<span>
<IconButton
color={row.favorite ? "warning" : "default"}
onClick={() => {
onFavorite([row.id], "favorite", !row.favorite);
row.favorite = !row.favorite;
}}
>
<StarIcon />
</IconButton>
</span>
</Tooltip>
</TableCell>
<TableCell sx={{ maxWidth: 100, overflow: "auto" }}>
<Box sx={{ maxHeight: 100, overflow: "auto" }}>{row.id}</Box>
</TableCell>
<TableCell sx={{ maxWidth: 200, overflow: "auto" }}>
<Box sx={{ maxHeight: 100, overflow: "auto" }}>{row.url}</Box>
</TableCell>
<TableCell sx={{ maxWidth: 150, overflow: "auto" }}>
<Box sx={{ maxHeight: 100, overflow: "auto" }}>
{JSON.stringify(row.elements)}
</Box>
</TableCell>
<TableCell sx={{ maxWidth: 150, overflow: "auto", padding: 0 }}>
<Accordion sx={{ margin: 0, padding: 0.5 }}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1a-content"
id="panel1a-header"
sx={{
minHeight: 0,
"&.Mui-expanded": { minHeight: 0 },
}}
>
<Box
sx={{
maxHeight: 150,
overflow: "auto",
width: "100%",
}}
>
<Typography sx={{ fontSize: "0.875rem" }}>
Show Result
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails sx={{ padding: 1 }}>
<Box sx={{ maxHeight: 200, overflow: "auto" }}>
<Typography
sx={{
fontSize: "0.875rem",
whiteSpace: "pre-wrap",
}}
>
{JSON.stringify(row.result, null, 2)}
</Typography>
</Box>
</AccordionDetails>
</Accordion>
</TableCell>
<TableCell sx={{ maxWidth: 150, overflow: "auto" }}>
<Box sx={{ maxHeight: 100, overflow: "auto" }}>
{new Date(row.time_created).toLocaleString()}
</Box>
</TableCell>
<TableCell sx={{ maxWidth: 150, overflow: "auto" }}>
<Box sx={{ maxHeight: 100, overflow: "auto" }}>
<Box
className="rounded-md p-2 text-center"
sx={{ bgcolor: colors[row.status], opactity: "50%" }}
>
{row.status}
</Box>
</Box>
</TableCell>
<TableCell sx={{ maxWidth: 100, overflow: "auto" }}>
<Button
onClick={() => {
onDownload([row.id]);
}}
>
Download
</Button>
<Button
onClick={() =>
onNavigate(row.elements, row.url, row.job_options)
}
>
Rerun
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
};

View File

@@ -0,0 +1,2 @@
export * from "./JobQueue";
export * from "./Favorites";

View File

@@ -7,8 +7,8 @@ const Jobs = () => {
const { user } = useAuth();
const [jobs, setJobs] = useState([]);
const fetchJobs = () => {
fetch("/api/retrieve-scrape-jobs", {
const fetchJobs = async () => {
await fetch("/api/retrieve-scrape-jobs", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ user: user?.email }),

View File

@@ -1,2 +1,3 @@
export * from "./element";
export * from "./result";
export * from "./job";

10
src/types/job.tsx Normal file
View File

@@ -0,0 +1,10 @@
export interface Job {
id: string;
url: string;
elements: Object[];
result: Object;
time_created: Date;
status: string;
job_options: Object;
favorite: boolean;
}