wip: update UI

This commit is contained in:
Jayden
2024-07-22 15:57:32 -05:00
parent cd3cd7eea2
commit 5b548060db
16 changed files with 221 additions and 116 deletions

View File

@@ -13,12 +13,16 @@ From the table, users can download an excel sheet of the job's results, along wi
## Features ## Features
### Submitting URLs for Scraping
- Submit/Queue URLs for web scraping - Submit/Queue URLs for web scraping
- Add and manage elements to scrape using XPath - Add and manage elements to scrape using XPath
- Scrape all pages within same domain - Scrape all pages within same domain
- Add custom json headers to send in requests to URLs - Add custom json headers to send in requests to URLs
- Display results of scraped data - Display results of scraped data
### Managing Previous Jobs
![main_page](https://github.com/jaypyles/www-scrape/blob/master/docs/main_page.png) ![main_page](https://github.com/jaypyles/www-scrape/blob/master/docs/main_page.png)
- Download csv containing results - Download csv containing results
@@ -28,14 +32,20 @@ From the table, users can download an excel sheet of the job's results, along wi
![job_page](https://github.com/jaypyles/www-scrape/blob/master/docs/job_page.png) ![job_page](https://github.com/jaypyles/www-scrape/blob/master/docs/job_page.png)
### User Management
- User login/signup to organize jobs - User login/signup to organize jobs
![login](https://github.com/jaypyles/www-scrape/blob/master/docs/login.png) ![login](https://github.com/jaypyles/www-scrape/blob/master/docs/login.png)
### Log Viewing
- View app logs inside of web ui - View app logs inside of web ui
![logs](https://github.com/jaypyles/www-scrape/blob/master/docs/log_page.png) ![logs](https://github.com/jaypyles/www-scrape/blob/master/docs/log_page.png)
### Statistics View
- View a small statistics view of jobs ran - View a small statistics view of jobs ran
![statistics](https://github.com/jaypyles/www-scrape/blob/master/docs/stats_page.png) ![statistics](https://github.com/jaypyles/www-scrape/blob/master/docs/stats_page.png)

View File

@@ -57,6 +57,7 @@ app.add_middleware(
) )
app.mount("/_next/static", StaticFiles(directory="./dist/_next/static"), name="static") app.mount("/_next/static", StaticFiles(directory="./dist/_next/static"), name="static")
app.mount("/images", StaticFiles(directory="./dist/images"), name="images")
@app.get("/") @app.get("/")

View File

@@ -5,6 +5,8 @@ services:
build: build:
context: ./ context: ./
container_name: scraperr container_name: scraperr
ports:
- 9000:8000
env_file: env_file:
- ./.env - ./.env
volumes: volumes:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -2,6 +2,7 @@
const nextConfig = { const nextConfig = {
output: "export", output: "export",
distDir: "./dist", distDir: "./dist",
images: { unoptimized: true },
async rewrites() { async rewrites() {
return [ return [
{ {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 454 B

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,4 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=https://cloudconvert.com/
HostUrl=https://us-east.storage.cloudconvert.com/tasks/ff0a6031-1745-4e41-871e-35f102bfe11b/scraperr_logo.ico?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=cloudconvert-production%2F20240722%2Fva%2Fs3%2Faws4_request&X-Amz-Date=20240722T165500Z&X-Amz-Expires=86400&X-Amz-Signature=df96cece92026dc08d5b1dbfc2391beffa5101137fade6587625c467f088feb7&X-Amz-SignedHeaders=host&response-content-disposition=attachment%3B%20filename%3D%22scraperr_logo.ico%22&response-content-type=image%2Fvnd.microsoft.icon&x-id=GetObject

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -32,6 +32,7 @@ const COLOR_MAP: ColorMap = {
Queued: "rgba(255,201,5,0.25)", Queued: "rgba(255,201,5,0.25)",
Scraping: "rgba(3,104,255,0.25)", Scraping: "rgba(3,104,255,0.25)",
Completed: "rgba(5,255,51,0.25)", Completed: "rgba(5,255,51,0.25)",
Failed: "rgba(214,0,25,0.25)",
}; };
const JobTable: React.FC<JobTableProps> = ({ jobs, fetchJobs }) => { const JobTable: React.FC<JobTableProps> = ({ jobs, fetchJobs }) => {

View File

@@ -24,6 +24,7 @@ import TerminalIcon from "@mui/icons-material/Terminal";
import BarChart from "@mui/icons-material/BarChart"; import BarChart from "@mui/icons-material/BarChart";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import Image from "next/image";
interface NavDrawerProps { interface NavDrawerProps {
toggleTheme: () => void; toggleTheme: () => void;
@@ -112,11 +113,9 @@ const NavDrawer: React.FC<NavDrawerProps> = ({ toggleTheme, isDarkMode }) => {
</Typography> </Typography>
<Button <Button
variant="contained" variant="contained"
color="primary"
onClick={logout} onClick={logout}
sx={{ sx={{
width: "100%", width: "100%",
color: theme.palette.mode === "light" ? "#000000" : "#ffffff",
}} }}
> >
Logout Logout
@@ -125,24 +124,22 @@ const NavDrawer: React.FC<NavDrawerProps> = ({ toggleTheme, isDarkMode }) => {
) : ( ) : (
<Button <Button
variant="contained" variant="contained"
color="primary"
onClick={() => router.push("/login")} onClick={() => router.push("/login")}
sx={{ sx={{
width: "100%", width: "100%",
color: theme.palette.mode === "light" ? "#000000" : "#ffffff",
}} }}
> >
Login Login
</Button> </Button>
)} )}
<Divider sx={{ marginTop: 2, marginBottom: 2 }}></Divider> <Divider sx={{ marginTop: 2, marginBottom: 2 }}></Divider>
<Accordion sx={{ padding: 0, width: "90%" }}> <Accordion sx={{ padding: 0, width: "90%", marginBottom: 1 }}>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMoreIcon />} expandIcon={<ExpandMoreIcon />}
aria-controls="panel1a-content" aria-controls="panel1a-content"
id="panel1a-header" id="panel1a-header"
> >
<Typography>Settings</Typography> <Typography>Quick Settings</Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<div className="flex flex-row mr-1"> <div className="flex flex-row mr-1">

View File

@@ -136,21 +136,24 @@ export const JobQueue = ({
{new Date(row.time_created).toLocaleString()} {new Date(row.time_created).toLocaleString()}
</Box> </Box>
</TableCell> </TableCell>
<TableCell sx={{ maxWidth: 150, overflow: "auto" }}> <TableCell sx={{ maxWidth: 50, overflow: "auto" }}>
<Box sx={{ maxHeight: 100, overflow: "auto" }}> <Box sx={{ maxHeight: 100, overflow: "auto" }}>
<Box <Box
className="rounded-md p-2 text-center" className="rounded-md p-2 text-center"
sx={{ bgcolor: colors[row.status], opactity: "50%" }} sx={{ bgcolor: colors[row.status] }}
> >
{row.status} {row.status}
</Box> </Box>
</Box> </Box>
</TableCell> </TableCell>
<TableCell sx={{ maxWidth: 100, overflow: "auto" }}> <TableCell sx={{ maxWidth: 150, overflow: "auto" }}>
<Box sx={{ display: "flex", gap: 1 }}>
<Button <Button
onClick={() => { onClick={() => {
onDownload([row.id]); onDownload([row.id]);
}} }}
size="small"
sx={{ minWidth: 0, padding: "4px 8px" }}
> >
Download Download
</Button> </Button>
@@ -158,9 +161,12 @@ export const JobQueue = ({
onClick={() => onClick={() =>
onNavigate(row.elements, row.url, row.job_options) onNavigate(row.elements, row.url, row.job_options)
} }
size="small"
sx={{ minWidth: 0, padding: "4px 8px" }}
> >
Rerun Rerun
</Button> </Button>
</Box>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@@ -33,7 +33,7 @@ export const ElementTable = ({ rows, setRows, submittedURL }: Props) => {
const handleAddRow = () => { const handleAddRow = () => {
const updatedRow = { ...newRow, url: submittedURL }; const updatedRow = { ...newRow, url: submittedURL };
setRows([...rows, updatedRow]); setRows([updatedRow, ...rows]);
setNewRow({ name: "", xpath: "", url: "" }); setNewRow({ name: "", xpath: "", url: "" });
}; };
@@ -44,23 +44,67 @@ export const ElementTable = ({ rows, setRows, submittedURL }: Props) => {
}) })
); );
}; };
return ( return (
<> <Box className="animate-fadeIn p-2" bgcolor="background.paper">
<Box display="flex" gap={2} marginBottom={2} className="items-center"> <Box className="text-center mb-4">
<Typography variant="h4" sx={{ marginBottom: 1 }}>
Elements to Scrape
</Typography>
<TableContainer
component={Box}
sx={{ maxHeight: "50%", overflow: "auto" }}
>
<div className="rounded-lg shadow-md border border-gray-300 overflow-hidden">
<Table
stickyHeader
className="mb-4"
sx={{
tableLayout: "fixed",
width: "100%",
"& .MuiTableCell-root": {
borderBottom: "1px solid #e0e0e0",
},
}}
>
<TableHead>
<TableRow>
<TableCell>
<Typography sx={{ fontWeight: "bold" }}>Name</Typography>
</TableCell>
<TableCell>
<Typography sx={{ fontWeight: "bold" }}>XPath</Typography>
</TableCell>
<TableCell>
<Typography sx={{ fontWeight: "bold" }}>Actions</Typography>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>
<TextField <TextField
label="Name" label="Name"
variant="outlined" variant="outlined"
fullWidth fullWidth
value={newRow.name} value={newRow.name}
onChange={(e) => setNewRow({ ...newRow, name: e.target.value })} onChange={(e) =>
setNewRow({ ...newRow, name: e.target.value })
}
/> />
</TableCell>
<TableCell>
<TextField <TextField
label="XPath" label="XPath"
variant="outlined" variant="outlined"
fullWidth fullWidth
value={newRow.xpath} value={newRow.xpath}
onChange={(e) => setNewRow({ ...newRow, xpath: e.target.value })} onChange={(e) =>
setNewRow({ ...newRow, xpath: e.target.value })
}
/> />
</TableCell>
<TableCell>
<Tooltip <Tooltip
title={ title={
newRow.xpath.length > 0 && newRow.name.length > 0 newRow.xpath.length > 0 && newRow.name.length > 0
@@ -78,35 +122,24 @@ export const ElementTable = ({ rows, setRows, submittedURL }: Props) => {
height: "40px", height: "40px",
width: "40px", width: "40px",
}} }}
disabled={!(newRow.xpath.length > 0 && newRow.name.length > 0)} disabled={
!(newRow.xpath.length > 0 && newRow.name.length > 0)
}
> >
<AddIcon <AddIcon
fontSize="inherit" fontSize="inherit"
sx={{ sx={{
color: theme.palette.mode === "light" ? "#000000" : "#ffffff", color:
theme.palette.mode === "light"
? "#000000"
: "#ffffff",
}} }}
/> />
</IconButton> </IconButton>
</span> </span>
</Tooltip> </Tooltip>
</Box>
<Typography variant="h4">Elements</Typography>
<TableContainer
component={Box}
sx={{ maxHeight: "50%", overflow: "auto" }}
>
<Table stickyHeader className="mb-4">
<TableHead>
<TableRow>
<TableCell>
<Typography sx={{ fontWeight: "bold" }}>Name</Typography>
</TableCell>
<TableCell>
<Typography sx={{ fontWeight: "bold" }}>XPath</Typography>
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableHead>
<TableBody>
{rows.map((row, index) => ( {rows.map((row, index) => (
<TableRow key={index}> <TableRow key={index}>
<TableCell> <TableCell>
@@ -116,7 +149,11 @@ export const ElementTable = ({ rows, setRows, submittedURL }: Props) => {
<Typography>{row.xpath}</Typography> <Typography>{row.xpath}</Typography>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Button onClick={() => handleDeleteRow(row.name)}> <Button
onClick={() => handleDeleteRow(row.name)}
className="!bg-red-500 bg-opacity-50 !text-white font-semibold rounded-md
transition-transform transform hover:scale-105 hover:bg-red-500"
>
Delete Delete
</Button> </Button>
</TableCell> </TableCell>
@@ -124,7 +161,9 @@ export const ElementTable = ({ rows, setRows, submittedURL }: Props) => {
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</div>
</TableContainer> </TableContainer>
</> </Box>
</Box>
); );
}; };

View File

@@ -15,7 +15,8 @@ interface StateProps {
submittedURL: string; submittedURL: string;
setSubmittedURL: Dispatch<React.SetStateAction<string>>; setSubmittedURL: Dispatch<React.SetStateAction<string>>;
rows: Element[]; rows: Element[];
setResults: Dispatch<React.SetStateAction<Result>>; isValidURL: boolean;
setIsValidUrl: Dispatch<React.SetStateAction<boolean>>;
setSnackbarMessage: Dispatch<React.SetStateAction<string>>; setSnackbarMessage: Dispatch<React.SetStateAction<string>>;
setSnackbarOpen: Dispatch<React.SetStateAction<boolean>>; setSnackbarOpen: Dispatch<React.SetStateAction<boolean>>;
setSnackbarSeverity: Dispatch<React.SetStateAction<string>>; setSnackbarSeverity: Dispatch<React.SetStateAction<string>>;
@@ -40,13 +41,13 @@ export const JobSubmitter = ({ stateProps }: Props) => {
submittedURL, submittedURL,
setSubmittedURL, setSubmittedURL,
rows, rows,
setResults, isValidURL,
setIsValidUrl,
setSnackbarMessage, setSnackbarMessage,
setSnackbarOpen, setSnackbarOpen,
setSnackbarSeverity, setSnackbarSeverity,
} = stateProps; } = stateProps;
const [isValidURL, setIsValidUrl] = useState<boolean>(true);
const [urlError, setUrlError] = useState<string | null>(null); const [urlError, setUrlError] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [jobOptions, setJobOptions] = useState<JobOptions>({ const [jobOptions, setJobOptions] = useState<JobOptions>({
@@ -157,15 +158,17 @@ export const JobSubmitter = ({ stateProps }: Props) => {
onChange={(e) => setSubmittedURL(e.target.value)} onChange={(e) => setSubmittedURL(e.target.value)}
error={!isValidURL} error={!isValidURL}
helperText={!isValidURL ? urlError : ""} helperText={!isValidURL ? urlError : ""}
className="rounded-md"
/> />
<Button <Button
variant="contained" variant="contained"
color="primary"
size="small" size="small"
onClick={handleSubmit} onClick={handleSubmit}
disabled={!(rows.length > 0) || loading} disabled={!(rows.length > 0) || loading}
className={`bg-gradient-to-r from-[#034efc] to-gray-500 text-white font-semibold rounded-md
transition-transform transform hover:scale-105 disabled:opacity-50`}
> >
{loading ? <CircularProgress size={24} /> : "Submit"} {loading ? <CircularProgress size={24} color="inherit" /> : "Submit"}
</Button> </Button>
</div> </div>
<Box bgcolor="background.paper" className="flex flex-col mb-2 rounded-md"> <Box bgcolor="background.paper" className="flex flex-col mb-2 rounded-md">

View File

@@ -14,6 +14,7 @@ const Home = () => {
const [snackbarOpen, setSnackbarOpen] = useState<boolean>(false); const [snackbarOpen, setSnackbarOpen] = useState<boolean>(false);
const [snackbarMessage, setSnackbarMessage] = useState<string>(""); const [snackbarMessage, setSnackbarMessage] = useState<string>("");
const [snackbarSeverity, setSnackbarSeverity] = useState<string>("error"); const [snackbarSeverity, setSnackbarSeverity] = useState<string>("error");
const [isValidURL, setIsValidUrl] = useState<boolean>(true);
const resultsRef = useRef<HTMLTableElement | null>(null); const resultsRef = useRef<HTMLTableElement | null>(null);
@@ -74,26 +75,26 @@ const Home = () => {
minHeight="100vh" minHeight="100vh"
py={4} py={4}
> >
<Container maxWidth="md"> <Container maxWidth="lg">
<Typography variant="h1" gutterBottom textAlign="center">
Scraperr
</Typography>
<JobSubmitter <JobSubmitter
stateProps={{ stateProps={{
submittedURL, submittedURL,
setSubmittedURL, setSubmittedURL,
rows, rows,
setResults, isValidURL,
setIsValidUrl,
setSnackbarMessage, setSnackbarMessage,
setSnackbarOpen, setSnackbarOpen,
setSnackbarSeverity, setSnackbarSeverity,
}} }}
/> />
{submittedURL.length ? (
<ElementTable <ElementTable
rows={rows} rows={rows}
setRows={setRows} setRows={setRows}
submittedURL={submittedURL} submittedURL={submittedURL}
/> />
) : null}
</Container> </Container>
{snackbarSeverity === "info" ? <NotifySnackbar /> : <ErrorSnackbar />} {snackbarSeverity === "info" ? <NotifySnackbar /> : <ErrorSnackbar />}
</Box> </Box>

View File

@@ -70,7 +70,24 @@ const lightTheme = createTheme({
secondary: "#333333", secondary: "#333333",
}, },
}, },
...commonThemeOptions, ...commonThemeOptions,
components: {
...commonThemeOptions.components,
MuiButton: {
styleOverrides: {
root: {
color: "white",
"&.MuiButton-root": {
backgroundColor: "#034efc",
},
"&:hover": {
backgroundColor: "#027be0",
},
},
},
},
},
}); });
const darkTheme = createTheme({ const darkTheme = createTheme({
@@ -132,6 +149,12 @@ const darkTheme = createTheme({
styleOverrides: { styleOverrides: {
root: { root: {
color: "white", color: "white",
"&.MuiButton-root": {
backgroundColor: "#034efc",
},
"&:hover": {
backgroundColor: "#027be0",
},
}, },
}, },
}, },

View File

@@ -1,6 +1,23 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"], content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {}, theme: {
extend: {
animation: {
fadeIn: "fadeIn 0.5s ease-in-out",
fadeOut: "fadeOut 0.5s ease-in-out",
},
keyframes: {
fadeIn: {
"0%": { opacity: 0 },
"100%": { opacity: 1 },
},
fadeOut: {
"0%": { opacity: 1 },
"100%": { opacity: 0 },
},
},
},
},
plugins: [], plugins: [],
}; };