Compare commits
2 Commits
23c58e20ae
...
d11c5acb77
| Author | SHA1 | Date | |
|---|---|---|---|
| d11c5acb77 | |||
| f63f4e5f10 |
@@ -1,21 +1,17 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
|
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||||
import AppLayout from "./components/AppLayout";
|
import AppLayout from "./components/AppLayout";
|
||||||
|
import DatasetStatusPage from "./pages/DatasetStatus";
|
||||||
import LoginPage from "./pages/Login";
|
import LoginPage from "./pages/Login";
|
||||||
import UploadPage from "./pages/Upload";
|
import UploadPage from "./pages/Upload";
|
||||||
import StatPage from "./pages/Stats";
|
import StatPage from "./pages/Stats";
|
||||||
|
import { getDocumentTitle } from "./utils/documentTitle";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const routeTitles: Record<string, string> = {
|
document.title = getDocumentTitle(location.pathname);
|
||||||
"/login": "Sign In",
|
|
||||||
"/upload": "Upload Dataset",
|
|
||||||
"/stats": "Stats",
|
|
||||||
};
|
|
||||||
|
|
||||||
document.title = routeTitles[location.pathname];
|
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -24,6 +20,7 @@ function App() {
|
|||||||
<Route path="/" element={<Navigate to="/login" replace />} />
|
<Route path="/" element={<Navigate to="/login" replace />} />
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="/upload" element={<UploadPage />} />
|
<Route path="/upload" element={<UploadPage />} />
|
||||||
|
<Route path="/dataset/:datasetId/status" element={<DatasetStatusPage />} />
|
||||||
<Route path="/stats" element={<StatPage />} />
|
<Route path="/stats" element={<StatPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
111
frontend/src/pages/DatasetStatus.tsx
Normal file
111
frontend/src/pages/DatasetStatus.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import StatsStyling from "../styles/stats_styling";
|
||||||
|
|
||||||
|
type DatasetStatusResponse = {
|
||||||
|
status?: "processing" | "complete" | "error";
|
||||||
|
status_message?: string | null;
|
||||||
|
completed_at?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StatsStyling;
|
||||||
|
const API_BASE_URL = "http://localhost:5000";
|
||||||
|
|
||||||
|
const DatasetStatusPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { datasetId } = useParams<{ datasetId: string }>();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [status, setStatus] = useState<DatasetStatusResponse["status"]>("processing");
|
||||||
|
const [statusMessage, setStatusMessage] = useState("");
|
||||||
|
const parsedDatasetId = useMemo(() => Number(datasetId), [datasetId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!Number.isInteger(parsedDatasetId) || parsedDatasetId <= 0) {
|
||||||
|
setLoading(false);
|
||||||
|
setStatus("error");
|
||||||
|
setStatusMessage("Invalid dataset id.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pollTimer: number | undefined;
|
||||||
|
|
||||||
|
const pollStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<DatasetStatusResponse>(
|
||||||
|
`${API_BASE_URL}/dataset/${parsedDatasetId}/status`
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextStatus = response.data.status ?? "processing";
|
||||||
|
setStatus(nextStatus);
|
||||||
|
setStatusMessage(String(response.data.status_message ?? ""));
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (nextStatus === "complete") {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
navigate("/stats", { replace: true });
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setLoading(false);
|
||||||
|
setStatus("error");
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const message = String(error.response?.data?.error || error.message || "Request failed");
|
||||||
|
setStatusMessage(message);
|
||||||
|
} else {
|
||||||
|
setStatusMessage("Unable to fetch dataset status.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void pollStatus();
|
||||||
|
pollTimer = window.setInterval(() => {
|
||||||
|
if (status !== "complete" && status !== "error") {
|
||||||
|
void pollStatus();
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (pollTimer) {
|
||||||
|
window.clearInterval(pollTimer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [navigate, parsedDatasetId, status]);
|
||||||
|
|
||||||
|
const isProcessing = loading || status === "processing";
|
||||||
|
const isError = status === "error";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.page}>
|
||||||
|
<div style={{ ...styles.container, maxWidth: 720 }}>
|
||||||
|
<div style={{ ...styles.card, marginTop: 28 }}>
|
||||||
|
<h1 style={{ margin: 0, fontSize: 28, color: "#111827" }}>
|
||||||
|
{isProcessing ? "Processing dataset..." : isError ? "Dataset processing failed" : "Dataset ready"}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p style={{ ...styles.sectionSubtitle, marginTop: 10 }}>
|
||||||
|
{isProcessing &&
|
||||||
|
"Your dataset is being analyzed. This page will redirect to stats automatically once complete."}
|
||||||
|
{isError && "There was an issue while processing your dataset. Please review the error details."}
|
||||||
|
{status === "complete" && "Processing complete. Redirecting to your stats now..."}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...styles.card,
|
||||||
|
marginTop: 12,
|
||||||
|
borderColor: isError ? "rgba(185, 28, 28, 0.28)" : "rgba(0,0,0,0.06)",
|
||||||
|
background: isError ? "#fff5f5" : "#ffffff",
|
||||||
|
color: isError ? "#991b1b" : "#374151",
|
||||||
|
boxShadow: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{statusMessage || (isProcessing ? "Waiting for updates from the worker queue..." : "No details provided.")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DatasetStatusPage;
|
||||||
@@ -46,12 +46,14 @@ const UploadPage = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const datasetId = Number(response.data.dataset_id);
|
||||||
|
|
||||||
setReturnMessage(
|
setReturnMessage(
|
||||||
`Upload queued successfully (dataset #${response.data.dataset_id}). Redirecting to insights...`
|
`Upload queued successfully (dataset #${datasetId}). Redirecting to processing status...`
|
||||||
);
|
);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate("/stats");
|
navigate(`/dataset/${datasetId}/status`);
|
||||||
}, 400);
|
}, 400);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
setHasError(true);
|
setHasError(true);
|
||||||
|
|||||||
15
frontend/src/utils/documentTitle.ts
Normal file
15
frontend/src/utils/documentTitle.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const DEFAULT_TITLE = "Ethnograph View";
|
||||||
|
|
||||||
|
const STATIC_TITLES: Record<string, string> = {
|
||||||
|
"/login": "Sign In",
|
||||||
|
"/upload": "Upload Dataset",
|
||||||
|
"/stats": "Stats",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDocumentTitle = (pathname: string) => {
|
||||||
|
if (pathname.includes("status")) {
|
||||||
|
return "Processing Dataset";
|
||||||
|
}
|
||||||
|
|
||||||
|
return STATIC_TITLES[pathname] ?? DEFAULT_TITLE;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user