Skip to content

Commit 11e7c6a

Browse files
committed
Add basic Grid
1 parent cf0a706 commit 11e7c6a

File tree

9 files changed

+373
-15
lines changed

9 files changed

+373
-15
lines changed

frontend/package-lock.json

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"dependencies": {
1616
"@ag-grid-community/core": "^31.0.2",
1717
"@ag-grid-community/react": "^31.0.2",
18+
"@ag-grid-community/styles": "^31.0.2",
1819
"@ag-grid-enterprise/advanced-filter": "^31.0.2",
1920
"@ag-grid-enterprise/core": "^31.0.2",
2021
"@ag-grid-enterprise/multi-filter": "^31.0.2",
@@ -29,6 +30,8 @@
2930
"@mui/material": "^5.14.19",
3031
"@mui/x-data-grid": "^6.18.2",
3132
"@sentry/react": "^7.93.0",
33+
"@tanstack/react-query": "^5.17.19",
34+
"luxon": "^3.4.4",
3235
"openapi-fetch": "^0.8.2",
3336
"react": "^18.2.0",
3437
"react-dom": "^18.2.0",
@@ -37,6 +40,7 @@
3740
"devDependencies": {
3841
"@sentry/vite-plugin": "^2.10.2",
3942
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
43+
"@types/luxon": "^3.4.2",
4044
"@types/node": "^20.11.5",
4145
"@types/react": "^18.2.48",
4246
"@types/react-dom": "^18.2.15",

frontend/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Sidebar from "./AppFrame";
44
import Error404 from "./routes/404";
55
import Home from "./routes/Home";
66
import ViewBatch from "./routes/batch/ViewBatch";
7+
import ViewJobs from "./routes/job/ViewJobs";
78

89
export default function App() {
910
const router = createBrowserRouter(
@@ -16,6 +17,10 @@ export default function App() {
1617
index: true,
1718
Component: Home,
1819
},
20+
{
21+
path: "job",
22+
Component: ViewJobs,
23+
},
1924
{
2025
path: "batch",
2126
},

frontend/src/AppFrame.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ function Sidebar() {
7171
<List>
7272
<ListItemLink to="/">Home</ListItemLink>
7373
<Divider />
74+
<SidebarSection title="Job">
75+
<ListItemLink to="/job">View Jobs</ListItemLink>
76+
</SidebarSection>
77+
<Divider />
7478
<SidebarSection title="Batch">
7579
<ListItemLink to="/batch">View All Batches</ListItemLink>
7680
<ListItemLink to="/batch/new">Create New Batch</ListItemLink>

frontend/src/main.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
1+
import { ModuleRegistry } from "@ag-grid-community/core";
2+
import "@ag-grid-community/styles/ag-grid.css";
3+
import "@ag-grid-community/styles/ag-theme-quartz.css";
4+
import { AdvancedFilterModule } from "@ag-grid-enterprise/advanced-filter";
5+
import { MultiFilterModule } from "@ag-grid-enterprise/multi-filter";
6+
import { RowGroupingModule } from "@ag-grid-enterprise/row-grouping";
7+
import { ServerSideRowModelModule } from "@ag-grid-enterprise/server-side-row-model";
8+
import { SetFilterModule } from "@ag-grid-enterprise/set-filter";
9+
import { StatusBarModule } from "@ag-grid-enterprise/status-bar";
110
import "@fontsource/roboto/300.css";
211
import "@fontsource/roboto/400.css";
312
import "@fontsource/roboto/500.css";
413
import "@fontsource/roboto/700.css";
14+
// Theme
515
import CssBaseline from "@mui/material/CssBaseline";
616
import * as Sentry from "@sentry/react";
17+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
718
import React from "react";
819
import ReactDOM from "react-dom/client";
920

@@ -25,9 +36,22 @@ Sentry.init({
2536
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
2637
});
2738

39+
ModuleRegistry.registerModules([
40+
AdvancedFilterModule,
41+
MultiFilterModule,
42+
RowGroupingModule,
43+
ServerSideRowModelModule,
44+
SetFilterModule,
45+
StatusBarModule,
46+
]);
47+
48+
const queryClient = new QueryClient();
49+
2850
ReactDOM.createRoot(document.getElementById("root")!).render(
2951
<React.StrictMode>
3052
<CssBaseline enableColorScheme={true} />
31-
<App />
53+
<QueryClientProvider client={queryClient}>
54+
<App />
55+
</QueryClientProvider>
3256
</React.StrictMode>,
3357
);

frontend/src/routes/job/ViewJobs.tsx

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import {
2+
ColDef,
3+
ColGroupDef,
4+
ICellRendererParams,
5+
IServerSideGetRowsParams,
6+
LoadSuccessParams,
7+
} from "@ag-grid-community/core";
8+
import { AgGridReact } from "@ag-grid-community/react";
9+
import { DateTime } from "luxon";
10+
11+
import { Job } from "../../api/api";
12+
import BatchChip from "../../misc/BatchChip";
13+
14+
export default function ViewJobs() {
15+
const retColDefs: [
16+
ColDef<Job, number>,
17+
ColDef<Job>,
18+
ColDef<Job, string | null>,
19+
] = [
20+
{
21+
field: "retry",
22+
headerName: "Retries Used",
23+
sortable: true,
24+
filter: true,
25+
},
26+
{
27+
valueGetter: (params) => 4 - (params.data?.retry ?? 0),
28+
headerName: "Retries Left",
29+
sortable: true,
30+
filter: true,
31+
},
32+
{
33+
field: "delayed_until",
34+
headerName: "Delayed Until",
35+
sortable: true,
36+
filter: true,
37+
},
38+
];
39+
const colDefs: [
40+
ColDef<Job, number>,
41+
ColDef<Job, string>,
42+
ColDef<Job, number[] | undefined>,
43+
ColDef<Job, string | null>,
44+
ColDef<Job, string | null>,
45+
ColGroupDef<Job>,
46+
ColDef<Job, string>,
47+
ColDef<Job, number>,
48+
] = [
49+
{ field: "id", headerName: "ID", sortable: true, filter: true },
50+
{ field: "url", headerName: "URL", sortable: true, filter: true },
51+
{
52+
field: "batches",
53+
headerName: "Batches",
54+
sortable: true,
55+
filter: true,
56+
cellRenderer(params: ICellRendererParams<Job, number[] | undefined>) {
57+
return (params.value ?? []).map((batchId) => (
58+
<>
59+
<BatchChip batchId={batchId} />
60+
<br />
61+
</>
62+
));
63+
},
64+
},
65+
{
66+
field: "completed",
67+
headerName: "Archive URL",
68+
sortable: true,
69+
filter: true,
70+
cellRenderer(params: ICellRendererParams<Job, string | null>) {
71+
if (params.value === null || params.value === undefined) {
72+
return null;
73+
}
74+
const date = DateTime.fromISO(params.value);
75+
const archiveURLString = `https://web.archive.org/web/${date.toFormat(
76+
"yyyyMMddHHmmss",
77+
)}/${params.data!.url}`;
78+
return <a href={archiveURLString}>{archiveURLString}</a>;
79+
},
80+
},
81+
{ field: "failed", headerName: "Failed At", sortable: true, filter: true },
82+
{
83+
headerName: "Retries",
84+
children: retColDefs,
85+
},
86+
{
87+
field: "created_at",
88+
headerName: "Created At",
89+
sortable: true,
90+
filter: true,
91+
},
92+
{ field: "priority", headerName: "Priority", sortable: true, filter: true },
93+
];
94+
95+
return (
96+
<div className="ag-theme-quartz" style={{ height: "80vh" }}>
97+
<AgGridReact
98+
rowModelType="serverSide"
99+
columnDefs={colDefs}
100+
serverSideDatasource={{
101+
getRows(params: IServerSideGetRowsParams<Job>) {
102+
fetch("/job/grid_sort", {
103+
body: JSON.stringify(params.request),
104+
method: "POST",
105+
headers: {
106+
"Content-Type": "application/json",
107+
},
108+
})
109+
.then((res) => res.json())
110+
.then((data: LoadSuccessParams) => params.success(data))
111+
.catch(() => params.fail());
112+
},
113+
}}
114+
/>
115+
</div>
116+
);
117+
}

src/main.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
TypeVar,
1616
overload,
1717
)
18+
from starlette.exceptions import HTTPException as StarletteHTTPException
1819

1920
import sqlalchemy
2021
import sqlalchemy.ext.asyncio
@@ -259,9 +260,23 @@ async def lifespan(_: FastAPI):
259260
await engine.dispose()
260261

261262

263+
class SPAStaticFiles(StaticFiles):
264+
async def get_response(self, path: str, scope) -> Response:
265+
try:
266+
response = await super().get_response(path, scope)
267+
except StarletteHTTPException as e:
268+
if e.status_code == 404:
269+
response = await super().get_response("./index.html", scope)
270+
else:
271+
raise
272+
if response.status_code == 404:
273+
response = await super().get_response("./index.html", scope)
274+
return response
275+
276+
262277
app = FastAPI(lifespan=lifespan)
263278
os.makedirs("frontend/dist", exist_ok=True)
264-
static_files = StaticFiles(directory="frontend/dist", html=True)
279+
static_files = SPAStaticFiles(directory="frontend/dist", html=True)
265280
app.mount("/app", static_files, name="frontend")
266281
app.add_middleware(
267282
CORSMiddleware,
@@ -271,19 +286,6 @@ async def lifespan(_: FastAPI):
271286
)
272287

273288

274-
# Serves /index.html if we are in /app and there is a 404
275-
@app.middleware("http")
276-
async def spa_middleware(
277-
req: Request, call_next: Callable[[Request], Awaitable[Response]]
278-
):
279-
resp = await call_next(req)
280-
if resp.status_code == 404 and req.url.path.startswith("/app"):
281-
# Note: Find a better way to do this!
282-
req.scope["path"] = "/app/index.html"
283-
resp = await call_next(req)
284-
return resp
285-
286-
287289
redirect_map: dict[str, str] = {
288290
"/queue_batch": "/queue/batch",
289291
"/queue_loop": "/queue/loop",

0 commit comments

Comments
 (0)