Editable and removable datasets #8
@@ -7,6 +7,7 @@ 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";
|
import { getDocumentTitle } from "./utils/documentTitle";
|
||||||
|
import DatasetEditPage from "./pages/DatasetEdit";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -24,6 +25,7 @@ function App() {
|
|||||||
<Route path="/datasets" element={<DatasetsPage />} />
|
<Route path="/datasets" element={<DatasetsPage />} />
|
||||||
<Route path="/dataset/:datasetId/status" element={<DatasetStatusPage />} />
|
<Route path="/dataset/:datasetId/status" element={<DatasetStatusPage />} />
|
||||||
<Route path="/dataset/:datasetId/stats" element={<StatPage />} />
|
<Route path="/dataset/:datasetId/stats" element={<StatPage />} />
|
||||||
|
<Route path="/dataset/:datasetId/edit" element={<DatasetEditPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|||||||
161
frontend/src/pages/DatasetEdit.tsx
Normal file
161
frontend/src/pages/DatasetEdit.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import StatsStyling from "../styles/stats_styling";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useEffect, useMemo, useState, type FormEvent } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL;
|
||||||
|
const styles = StatsStyling;
|
||||||
|
|
||||||
|
type DatasetInfoResponse = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DatasetEditPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { datasetId } = useParams<{ datasetId: string }>();
|
||||||
|
const parsedDatasetId = useMemo(() => Number(datasetId), [datasetId]);
|
||||||
|
const [statusMessage, setStatusMessage] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [hasError, setHasError] = useState(false);
|
||||||
|
|
||||||
|
const [datasetName, setDatasetName] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!Number.isInteger(parsedDatasetId) || parsedDatasetId <= 0) {
|
||||||
|
setHasError(true);
|
||||||
|
setStatusMessage("Invalid dataset id.");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
if (!token) {
|
||||||
|
setHasError(true);
|
||||||
|
setStatusMessage("You must be signed in to edit datasets.");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get<DatasetInfoResponse>(`${API_BASE_URL}/dataset/${parsedDatasetId}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
setDatasetName(response.data.name || "");
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
setHasError(true);
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
setStatusMessage(String(error.response?.data?.error || error.message));
|
||||||
|
} else {
|
||||||
|
setStatusMessage("Could not get dataset info.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [parsedDatasetId]);
|
||||||
|
|
||||||
|
const saveDatasetName = async (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const trimmedName = datasetName.trim();
|
||||||
|
if (!trimmedName) {
|
||||||
|
setHasError(true);
|
||||||
|
setStatusMessage("Please enter a valid dataset name.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
if (!token) {
|
||||||
|
setHasError(true);
|
||||||
|
setStatusMessage("You must be signed in to save changes.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSaving(true);
|
||||||
|
setHasError(false);
|
||||||
|
setStatusMessage("");
|
||||||
|
|
||||||
|
await axios.patch(
|
||||||
|
`${API_BASE_URL}/dataset/${parsedDatasetId}`,
|
||||||
|
{ name: trimmedName },
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
|
||||||
|
navigate("/datasets", { replace: true });
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setHasError(true);
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
setStatusMessage(String(error.response?.data?.error || error.message || "Save failed."));
|
||||||
|
} else {
|
||||||
|
setStatusMessage("Save failed due to an unexpected error.");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.page}>
|
||||||
|
<div style={styles.containerNarrow}>
|
||||||
|
<div style={{ ...styles.card, ...styles.headerBar }}>
|
||||||
|
<div>
|
||||||
|
<h1 style={styles.sectionHeaderTitle}>Edit Dataset</h1>
|
||||||
|
<p style={styles.sectionHeaderSubtitle}>Update the dataset name shown in your datasets list.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={saveDatasetName}
|
||||||
|
style={{ ...styles.card, marginTop: 14, display: "grid", gap: 12 }}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
htmlFor="dataset-name"
|
||||||
|
style={{ fontSize: 13, color: "#374151", fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
Dataset name
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="dataset-name"
|
||||||
|
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||||
|
type="text"
|
||||||
|
placeholder="Example: Cork Discussions - Jan 2026"
|
||||||
|
value={datasetName}
|
||||||
|
onChange={(event) => setDatasetName(event.target.value)}
|
||||||
|
disabled={loading || isSaving}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style={styles.buttonSecondary}
|
||||||
|
onClick={() => navigate("/datasets")}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
style={{ ...styles.buttonPrimary, opacity: loading || isSaving ? 0.75 : 1 }}
|
||||||
|
disabled={loading || isSaving}
|
||||||
|
>
|
||||||
|
{isSaving ? "Saving..." : "Save"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{loading
|
||||||
|
? "Loading dataset details..."
|
||||||
|
: statusMessage}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DatasetEditPage;
|
||||||
@@ -94,6 +94,7 @@ const DatasetsPage = () => {
|
|||||||
<ul style={styles.listNoBullets}>
|
<ul style={styles.listNoBullets}>
|
||||||
{datasets.map((dataset) => {
|
{datasets.map((dataset) => {
|
||||||
const isComplete = dataset.status === "complete";
|
const isComplete = dataset.status === "complete";
|
||||||
|
const editPath = `/dataset/${dataset.id}/edit`;
|
||||||
const targetPath = isComplete
|
const targetPath = isComplete
|
||||||
? `/dataset/${dataset.id}/stats`
|
? `/dataset/${dataset.id}/stats`
|
||||||
: `/dataset/${dataset.id}/status`;
|
: `/dataset/${dataset.id}/status`;
|
||||||
@@ -117,6 +118,17 @@ const DatasetsPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{ isComplete &&
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style={{...styles.buttonSecondary, "margin": "5px"}}
|
||||||
|
onClick={() => navigate(editPath)}
|
||||||
|
>
|
||||||
|
Edit Dataset
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
style={isComplete ? styles.buttonPrimary : styles.buttonSecondary}
|
style={isComplete ? styles.buttonPrimary : styles.buttonSecondary}
|
||||||
@@ -124,6 +136,7 @@ const DatasetsPage = () => {
|
|||||||
>
|
>
|
||||||
{isComplete ? "Open stats" : "View status"}
|
{isComplete ? "Open stats" : "View status"}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -164,10 +164,10 @@ def get_dataset(dataset_id):
|
|||||||
if not dataset_manager.authorize_user_dataset(dataset_id, user_id):
|
if not dataset_manager.authorize_user_dataset(dataset_id, user_id):
|
||||||
raise NotAuthorisedException("This user is not authorised to access this dataset")
|
raise NotAuthorisedException("This user is not authorised to access this dataset")
|
||||||
|
|
||||||
dataset_content = dataset_manager.get_dataset_content(dataset_id)
|
dataset_info = dataset_manager.get_dataset_info(dataset_id)
|
||||||
filters = get_request_filters()
|
included_cols = {"id", "name", "created_at"}
|
||||||
filtered_dataset = stat_gen.filter_dataset(dataset_content, filters)
|
|
||||||
return jsonify(filtered_dataset), 200
|
return jsonify({k: dataset_info[k] for k in included_cols}), 200
|
||||||
except NotAuthorisedException:
|
except NotAuthorisedException:
|
||||||
return jsonify({"error": "User is not authorised to access this content"}), 403
|
return jsonify({"error": "User is not authorised to access this content"}), 403
|
||||||
except NonExistentDatasetException:
|
except NonExistentDatasetException:
|
||||||
@@ -253,6 +253,8 @@ def content_endpoint(dataset_id):
|
|||||||
return jsonify(stat_gen.get_content_analysis(dataset_content, filters)), 200
|
return jsonify(stat_gen.get_content_analysis(dataset_content, filters)), 200
|
||||||
except NotAuthorisedException:
|
except NotAuthorisedException:
|
||||||
return jsonify({"error": "User is not authorised to access this content"}), 403
|
return jsonify({"error": "User is not authorised to access this content"}), 403
|
||||||
|
except NonExistentDatasetException:
|
||||||
|
return jsonify({"error": "Dataset does not exist"}), 404
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400
|
return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -273,6 +275,8 @@ def get_summary(dataset_id):
|
|||||||
return jsonify(stat_gen.summary(dataset_content, filters)), 200
|
return jsonify(stat_gen.summary(dataset_content, filters)), 200
|
||||||
except NotAuthorisedException:
|
except NotAuthorisedException:
|
||||||
return jsonify({"error": "User is not authorised to access this content"}), 403
|
return jsonify({"error": "User is not authorised to access this content"}), 403
|
||||||
|
except NonExistentDatasetException:
|
||||||
|
return jsonify({"error": "Dataset does not exist"}), 404
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400
|
return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -293,6 +297,8 @@ def get_time_analysis(dataset_id):
|
|||||||
return jsonify(stat_gen.get_time_analysis(dataset_content, filters)), 200
|
return jsonify(stat_gen.get_time_analysis(dataset_content, filters)), 200
|
||||||
except NotAuthorisedException:
|
except NotAuthorisedException:
|
||||||
return jsonify({"error": "User is not authorised to access this content"}), 403
|
return jsonify({"error": "User is not authorised to access this content"}), 403
|
||||||
|
except NonExistentDatasetException:
|
||||||
|
return jsonify({"error": "Dataset does not exist"}), 404
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400
|
return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -313,6 +319,8 @@ def get_user_analysis(dataset_id):
|
|||||||
return jsonify(stat_gen.get_user_analysis(dataset_content, filters)), 200
|
return jsonify(stat_gen.get_user_analysis(dataset_content, filters)), 200
|
||||||
except NotAuthorisedException:
|
except NotAuthorisedException:
|
||||||
return jsonify({"error": "User is not authorised to access this content"}), 403
|
return jsonify({"error": "User is not authorised to access this content"}), 403
|
||||||
|
except NonExistentDatasetException:
|
||||||
|
return jsonify({"error": "Dataset does not exist"}), 404
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400
|
return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -333,6 +341,8 @@ def get_cultural_analysis(dataset_id):
|
|||||||
return jsonify(stat_gen.get_cultural_analysis(dataset_content, filters)), 200
|
return jsonify(stat_gen.get_cultural_analysis(dataset_content, filters)), 200
|
||||||
except NotAuthorisedException:
|
except NotAuthorisedException:
|
||||||
return jsonify({"error": "User is not authorised to access this content"}), 403
|
return jsonify({"error": "User is not authorised to access this content"}), 403
|
||||||
|
except NonExistentDatasetException:
|
||||||
|
return jsonify({"error": "Dataset does not exist"}), 404
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400
|
return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -353,6 +363,8 @@ def get_interaction_analysis(dataset_id):
|
|||||||
return jsonify(stat_gen.get_interactional_analysis(dataset_content, filters)), 200
|
return jsonify(stat_gen.get_interactional_analysis(dataset_content, filters)), 200
|
||||||
except NotAuthorisedException:
|
except NotAuthorisedException:
|
||||||
return jsonify({"error": "User is not authorised to access this content"}), 403
|
return jsonify({"error": "User is not authorised to access this content"}), 403
|
||||||
|
except NonExistentDatasetException:
|
||||||
|
return jsonify({"error": "Dataset does not exist"}), 404
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400
|
return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user