HOKPVOTWZFYXYNKLYBD6XCRSKGNBRABIY5LBU7A5I7F6K7E45MNQC 3BOFJXKAMRCAQSJQD3AZWYG64A3IO54F4773ZOVWHJSVOQY34CSQC YX7LU4WRAUDMWS3DEDXZDSF6DXBHLYDWVSMSRK6KIW3MO6GRXSVQC M3JUJ2WWZGCVMBITKRM5FUJMHFYL2QRMXJUVRUE4AC2RF74AOL5AC EUEH65HBT4XZXCNWECJXNDEQAWR2NLNSAXFPXLMQ27NOVMQBJT5QC O7W4FZVRKDQDAAXEW4T7P262PPRILRCSSACODMUTQZ6VNR36PVCQC 6W73J6WJQHALRF4ZLBJ4JH6H7UYT74KFPKFH537T2BRW2G6MCLRQC DBDM3MJDJ5TRAH3FA7MLBGFUPZF4GIM4CPZAKFNS5MV3UYQOAFNQC 3IXYDVZ3CQBTNPSCYE3ZO7FVBAYF25HX5BAYEUERHYU3GRYZ7TRAC UOADDPQMMLEBR3F3WXOATJ32MZA742VEOQZJWYUBK7H4XLLVHMBQC import { createBrowserRouter } from "react-router-dom";import { AuthorisedLayout } from "../components/AuthorisedLayout";import { DatasetsPage } from "../pages/DatasetsPage";import { DatasetPage } from "../pages/DatasetPage";import { ClustersPage } from "../pages/ClustersPage";import { ClusterPage } from "../pages/ClusterPage";import { SelectionsPage } from "../pages/SelectionsPage";import { FilePage } from "../pages/FilePage";export const router = createBrowserRouter([{path: "/",element: <AuthorisedLayout />,children: [{path: "/",element: <DatasetsPage />,},{path: "/datasets",element: <DatasetsPage />,},{path: "/datasets/:datasetId",element: <DatasetPage />,},{path: "/datasets/:datasetId/locations/:locationId/clusters",element: <ClustersPage />,},{path: "/datasets/:datasetId/locations/:locationId/clusters/:clusterId",element: <ClusterPage />,},{path: "/datasets/:datasetId/species/:speciesId/selections",element: <SelectionsPage />,},{path: "/datasets/:datasetId/locations/:locationId/clusters/:clusterId/files/:fileId",element: <FilePage />,},],},]);
import React, { useEffect, useState } from "react";import { useParams, useLocation } from "react-router-dom";import { useKindeAuth } from "@kinde-oss/kinde-auth-react";import Selection from "../components/Selection";export const SelectionsPage: React.FC = () => {const { datasetId, speciesId } = useParams<{ datasetId: string; speciesId: string }>();const location = useLocation();const { getAccessToken } = useKindeAuth();const [datasetName, setDatasetName] = useState<string>("");const [loading, setLoading] = useState(true);useEffect(() => {const fetchInfo = async () => {if (!speciesId || !datasetId) return;// Try to get from location state firstconst state = location.state as { speciesName?: string } | null;if (state?.speciesName) {setLoading(false);return;}// Otherwise fetch from APItry {const accessToken = await getAccessToken();// Fetch species infoconst speciesResponse = await fetch(`/api/species/${speciesId}`, {headers: { Authorization: `Bearer ${accessToken}` },});if (speciesResponse.ok) {// Species name is not used in this component}// Fetch dataset infoconst datasetResponse = await fetch(`/api/datasets/${datasetId}`, {headers: { Authorization: `Bearer ${accessToken}` },});if (datasetResponse.ok) {const dataset = await datasetResponse.json();setDatasetName(dataset.name);}} catch (error) {console.error("Failed to fetch info:", error);} finally {setLoading(false);}};void fetchInfo();}, [datasetId, speciesId, getAccessToken, location.state]);const handleSpeciesFilterChange = () => {// Note: We're not changing the URL here since the route structure// expects a specific species ID. This maintains current behavior.};if (!datasetId || !speciesId) {return (<div className="p-4 bg-red-50 text-red-700 rounded-md"><p>Missing required parameters.</p></div>);}if (loading) {return <div className="p-4 text-center">Loading selections...</div>;}return (<div className="space-y-4"><SelectiondatasetId={datasetId}datasetName={datasetName}speciesId={speciesId}onSpeciesFilterChange={handleSpeciesFilterChange}/></div>);};
import React, { useEffect, useState } from "react";import { useParams, useNavigate } from "react-router-dom";import { useKindeAuth } from "@kinde-oss/kinde-auth-react";import File from "../components/File";import { File as FileType } from "../types/file";export const FilePage: React.FC = () => {const { datasetId, locationId, clusterId, fileId } = useParams<{datasetId: string;locationId: string;clusterId: string;fileId: string;}>();const navigate = useNavigate();const { getAccessToken } = useKindeAuth();const [file, setFile] = useState<FileType | null>(null);const [datasetOwnerId, setDatasetOwnerId] = useState<string | null>(null);const [permissions, setPermissions] = useState<string[]>([]);const [loading, setLoading] = useState(true);useEffect(() => {const fetchFileInfo = async () => {if (!fileId || !datasetId) return;try {const accessToken = await getAccessToken();// Fetch file infoconst fileResponse = await fetch(`/api/files/${fileId}`, {headers: { Authorization: `Bearer ${accessToken}` },});if (fileResponse.ok) {const fileData = await fileResponse.json();setFile(fileData);}// Fetch dataset info for permissionsconst datasetResponse = await fetch(`/api/datasets/${datasetId}`, {headers: { Authorization: `Bearer ${accessToken}` },});if (datasetResponse.ok) {const dataset = await datasetResponse.json();setDatasetOwnerId(dataset.ownerId);setPermissions(dataset.permissions || []);}} catch (error) {console.error("Failed to fetch file info:", error);} finally {setLoading(false);}};void fetchFileInfo();}, [fileId, datasetId, getAccessToken]);const handleClose = () => {// Navigate back to the cluster pagenavigate(`/datasets/${datasetId}/locations/${locationId}/clusters/${clusterId}`);};if (!datasetId || !locationId || !clusterId || !fileId) {return (<div className="p-4 bg-red-50 text-red-700 rounded-md"><p>Missing required parameters.</p></div>);}if (loading) {return <div className="p-4 text-center">Loading file...</div>;}if (!file) {return (<div className="p-4 bg-red-50 text-red-700 rounded-md"><p>File not found.</p></div>);}return (<Filefile={file}isOpen={true}onClose={handleClose}datasetOwnerId={datasetOwnerId}datasetId={datasetId}canEditSelections={permissions.includes('EDIT')}/>);};
import React from "react";import { useNavigate } from "react-router-dom";import Datasets from "../components/Datasets";export const DatasetsPage: React.FC = () => {const navigate = useNavigate();const handleDatasetSelect = (datasetId: string, datasetName: string, permissions?: string[]) => {navigate(`/datasets/${datasetId}`, {state: { datasetName, permissions }});};return (<div className="card p-6 bg-white shadow-sm rounded-lg"><Datasets onDatasetSelect={handleDatasetSelect} /></div>);};
import React, { useEffect, useState } from "react";import { useParams, useNavigate, useLocation } from "react-router-dom";import { useKindeAuth } from "@kinde-oss/kinde-auth-react";import Dataset from "../components/Dataset";export const DatasetPage: React.FC = () => {const { datasetId } = useParams<{ datasetId: string }>();const navigate = useNavigate();const location = useLocation();const { getAccessToken } = useKindeAuth();const [datasetName, setDatasetName] = useState<string>("");const [permissions, setPermissions] = useState<string[]>([]);const [loading, setLoading] = useState(true);useEffect(() => {const fetchDatasetInfo = async () => {if (!datasetId) return;// Try to get from location state firstconst state = location.state as { datasetName?: string; permissions?: string[] } | null;if (state?.datasetName) {setDatasetName(state.datasetName);setPermissions(state.permissions || []);setLoading(false);return;}// Otherwise fetch from APItry {const accessToken = await getAccessToken();const response = await fetch(`/api/datasets/${datasetId}`, {headers: { Authorization: `Bearer ${accessToken}` },});if (response.ok) {const dataset = await response.json();setDatasetName(dataset.name);setPermissions(dataset.permissions || []);}} catch (error) {console.error("Failed to fetch dataset info:", error);} finally {setLoading(false);}};void fetchDatasetInfo();}, [datasetId, getAccessToken, location.state]);const handleLocationSelect = (locationId: string, locationName: string) => {navigate(`/datasets/${datasetId}/locations/${locationId}/clusters`, {state: { locationName }});};const handleSpeciesSelect = (speciesId: string, speciesName: string) => {navigate(`/datasets/${datasetId}/species/${speciesId}/selections`, {state: { speciesName }});};if (!datasetId) {return (<div className="p-4 bg-red-50 text-red-700 rounded-md"><p>No dataset ID provided.</p></div>);}if (loading) {return <div className="p-4 text-center">Loading dataset...</div>;}return (<div className="space-y-4"><DatasetdatasetId={datasetId}datasetName={datasetName}onLocationSelect={handleLocationSelect}onSpeciesSelect={handleSpeciesSelect}canCreateLocations={permissions.includes('EDIT')}/></div>);};
import React, { useEffect, useState } from "react";import { useParams, useNavigate, useLocation } from "react-router-dom";import { useKindeAuth } from "@kinde-oss/kinde-auth-react";import Clusters from "../components/Clusters";export const ClustersPage: React.FC = () => {const { datasetId, locationId } = useParams<{ datasetId: string; locationId: string }>();const navigate = useNavigate();const location = useLocation();const { getAccessToken } = useKindeAuth();const [locationName, setLocationName] = useState<string>("");const [permissions, setPermissions] = useState<string[]>([]);const [loading, setLoading] = useState(true);useEffect(() => {const fetchInfo = async () => {if (!locationId || !datasetId) return;// Try to get from location state firstconst state = location.state as { locationName?: string } | null;if (state?.locationName) {setLocationName(state.locationName);setLoading(false);return;}// Otherwise fetch from APItry {const accessToken = await getAccessToken();// Fetch location infoconst locationResponse = await fetch(`/api/locations/${locationId}`, {headers: { Authorization: `Bearer ${accessToken}` },});if (locationResponse.ok) {const locationData = await locationResponse.json();setLocationName(locationData.name);}// Fetch dataset permissionsconst datasetResponse = await fetch(`/api/datasets/${datasetId}`, {headers: { Authorization: `Bearer ${accessToken}` },});if (datasetResponse.ok) {const dataset = await datasetResponse.json();setPermissions(dataset.permissions || []);}} catch (error) {console.error("Failed to fetch info:", error);} finally {setLoading(false);}};void fetchInfo();}, [datasetId, locationId, getAccessToken, location.state]);const handleClusterSelect = (clusterId: string, clusterName: string) => {navigate(`/datasets/${datasetId}/locations/${locationId}/clusters/${clusterId}`, {state: { clusterName }});};if (!datasetId || !locationId) {return (<div className="p-4 bg-red-50 text-red-700 rounded-md"><p>Missing required parameters.</p></div>);}if (loading) {return <div className="p-4 text-center">Loading clusters...</div>;}return (<div className="space-y-4"><ClusterslocationId={locationId}locationName={locationName}datasetId={datasetId}hideDatasetName={true}onClusterSelect={handleClusterSelect}canCreateClusters={permissions.includes('EDIT')}/></div>);};
import React, { useEffect, useState } from "react";import { useParams, useLocation, useSearchParams } from "react-router-dom";import { useKindeAuth } from "@kinde-oss/kinde-auth-react";import Cluster from "../components/Cluster";import { NightFilter } from "../types";export const ClusterPage: React.FC = () => {const { datasetId, locationId, clusterId } = useParams<{datasetId: string;locationId: string;clusterId: string;}>();const location = useLocation();const [searchParams] = useSearchParams();const { getAccessToken } = useKindeAuth();const [clusterName, setClusterName] = useState<string>("");const [loading, setLoading] = useState(true);// Get filter from URL paramsconst nightFilter = (searchParams.get("nightFilter") as NightFilter) || "none";useEffect(() => {const fetchClusterInfo = async () => {if (!clusterId) return;// Try to get from location state firstconst state = location.state as { clusterName?: string } | null;if (state?.clusterName) {setClusterName(state.clusterName);setLoading(false);return;}// Otherwise fetch from APItry {const accessToken = await getAccessToken();const response = await fetch(`/api/clusters/${clusterId}`, {headers: { Authorization: `Bearer ${accessToken}` },});if (response.ok) {const cluster = await response.json();setClusterName(cluster.name);}} catch (error) {console.error("Failed to fetch cluster info:", error);} finally {setLoading(false);}};void fetchClusterInfo();}, [clusterId, getAccessToken, location.state]);if (!datasetId || !locationId || !clusterId) {return (<div className="p-4 bg-red-50 text-red-700 rounded-md"><p>Missing required parameters.</p></div>);}if (loading) {return <div className="p-4 text-center">Loading cluster...</div>;}return (<div className="space-y-4"><ClusterclusterId={clusterId}clusterName={clusterName}locationId={locationId}datasetId={datasetId}hideHeaderInfo={true}nightFilter={nightFilter}/></div>);};
import React, { useEffect, useState } from "react";import { useLocation, useParams, useNavigate } from "react-router-dom";import { useKindeAuth } from "@kinde-oss/kinde-auth-react";import {Breadcrumb,BreadcrumbItem,BreadcrumbLink,BreadcrumbList,BreadcrumbPage,BreadcrumbSeparator} from "./ui/breadcrumb";interface BreadcrumbData {datasetName?: string;locationName?: string;clusterName?: string;speciesName?: string;fileName?: string;}export const RouteBreadcrumb: React.FC = () => {const location = useLocation();const params = useParams();const navigate = useNavigate();const { getAccessToken } = useKindeAuth();const [breadcrumbData, setBreadcrumbData] = useState<BreadcrumbData>({});const [loading, setLoading] = useState(false);// Fetch breadcrumb data based on current routeuseEffect(() => {const fetchBreadcrumbData = async () => {if (!params.datasetId) return;setLoading(true);try {const accessToken = await getAccessToken();const data: BreadcrumbData = {};// Fetch dataset nameif (params.datasetId) {const datasetResponse = await fetch(`/api/datasets/${params.datasetId}`, {headers: { Authorization: `Bearer ${accessToken}` },});if (datasetResponse.ok) {const dataset = await datasetResponse.json();data.datasetName = dataset.name;}}// Fetch location nameif (params.locationId) {const locationResponse = await fetch(`/api/locations/${params.locationId}`, {headers: { Authorization: `Bearer ${accessToken}` },});if (locationResponse.ok) {const location = await locationResponse.json();data.locationName = location.name;}}// Fetch cluster nameif (params.clusterId) {const clusterResponse = await fetch(`/api/clusters/${params.clusterId}`, {headers: { Authorization: `Bearer ${accessToken}` },});if (clusterResponse.ok) {const cluster = await clusterResponse.json();data.clusterName = cluster.name;}}// Fetch species nameif (params.speciesId) {const speciesResponse = await fetch(`/api/species/${params.speciesId}`, {headers: { Authorization: `Bearer ${accessToken}` },});if (speciesResponse.ok) {const species = await speciesResponse.json();data.speciesName = species.name;}}// Fetch file nameif (params.fileId) {const fileResponse = await fetch(`/api/files/${params.fileId}`, {headers: { Authorization: `Bearer ${accessToken}` },});if (fileResponse.ok) {const file = await fileResponse.json();data.fileName = file.fileName;}}setBreadcrumbData(data);} catch (error) {console.error("Failed to fetch breadcrumb data:", error);} finally {setLoading(false);}};void fetchBreadcrumbData();}, [params, getAccessToken]);// Don't render breadcrumb on datasets pageif (location.pathname === "/" || location.pathname === "/datasets") {return null;}if (loading) {return (<div className="flex justify-between items-center py-1"><div className="text-sm text-gray-500">Loading...</div></div>);}return (<div className="flex justify-between items-center py-1"><Breadcrumb className="text-sm py-0"><BreadcrumbList className="py-0"><BreadcrumbItem><BreadcrumbLinkonClick={() => navigate("/datasets")}className="cursor-pointer">Datasets</BreadcrumbLink></BreadcrumbItem>{breadcrumbData.datasetName && (<><BreadcrumbSeparator /><BreadcrumbItem>{params.locationId || params.speciesId ? (<BreadcrumbLinkonClick={() => navigate(`/datasets/${params.datasetId}`)}className="cursor-pointer">{breadcrumbData.datasetName}</BreadcrumbLink>) : (<BreadcrumbPage>{breadcrumbData.datasetName}</BreadcrumbPage>)}</BreadcrumbItem></>)}{breadcrumbData.locationName && (<><BreadcrumbSeparator /><BreadcrumbItem>{params.clusterId ? (<BreadcrumbLinkonClick={() => navigate(`/datasets/${params.datasetId}/locations/${params.locationId}/clusters`)}className="cursor-pointer">{breadcrumbData.locationName}</BreadcrumbLink>) : (<BreadcrumbPage>{breadcrumbData.locationName}</BreadcrumbPage>)}</BreadcrumbItem></>)}{breadcrumbData.clusterName && (<><BreadcrumbSeparator /><BreadcrumbItem>{params.fileId ? (<BreadcrumbLinkonClick={() => navigate(`/datasets/${params.datasetId}/locations/${params.locationId}/clusters/${params.clusterId}`)}className="cursor-pointer">{breadcrumbData.clusterName}</BreadcrumbLink>) : (<BreadcrumbPage>{breadcrumbData.clusterName}</BreadcrumbPage>)}</BreadcrumbItem></>)}{breadcrumbData.speciesName && (<><BreadcrumbSeparator /><BreadcrumbItem><BreadcrumbPage>{breadcrumbData.speciesName}</BreadcrumbPage></BreadcrumbItem></>)}{breadcrumbData.fileName && (<><BreadcrumbSeparator /><BreadcrumbItem><BreadcrumbPage>{breadcrumbData.fileName}</BreadcrumbPage></BreadcrumbItem></>)}</BreadcrumbList></Breadcrumb></div>);};
// File modal stateconst [selectedFile, setSelectedFile] = useState<FileType | null>(null);const [isFileModalOpen, setIsFileModalOpen] = useState<boolean>(false);const [datasetOwnerId, setDatasetOwnerId] = useState<string | null>(null);const [datasetPermissions, setDatasetPermissions] = useState<string[]>([]);
setSelectedFile(file);setIsFileModalOpen(true);// Fetch dataset owner and permissions if we don't have them yetif ((!datasetOwnerId || datasetPermissions.length === 0) && requiredParams.datasetId) {void fetchDatasetOwner(requiredParams.datasetId).catch(console.error);
// For files endpoint, navigate to file pageif (apiEndpoint === "files" && requiredParams.clusterId && requiredParams.datasetId && locationId) {navigate(`/datasets/${requiredParams.datasetId}/locations/${locationId}/clusters/${requiredParams.clusterId}/files/${file.id}`);
try {const accessToken = await getAccessToken();const response = await fetch(`/api/datasets/${targetDatasetId}`, {headers: {Authorization: `Bearer ${accessToken}`,},});if (response.ok) {const result = await response.json();const owner = result.data?.owner || result.owner || null;const permissions = result.data?.permissions || result.permissions || [];setDatasetOwnerId(owner);setDatasetPermissions(permissions);}} catch (error) {console.error("Error fetching dataset owner and permissions:", error);}}, [isAuthenticated, getAccessToken]);
// Fetch dataset permissions when component mounts or dataset changesuseEffect(() => {const targetDatasetId = requiredParams.datasetId || datasetId;if (isAuthenticated && !authLoading && targetDatasetId && datasetPermissions.length === 0) {void fetchDatasetOwner(targetDatasetId).catch(console.error);}}, [isAuthenticated, authLoading, requiredParams.datasetId, datasetId, datasetPermissions.length, fetchDatasetOwner]);
{/* File Modal */}<Filefile={selectedFile}isOpen={isFileModalOpen}onClose={() => setIsFileModalOpen(false)}datasetOwnerId={datasetOwnerId}datasetId={requiredParams.datasetId || datasetId || null}canEditSelections={(() => {const canEdit = datasetPermissions.includes('EDIT');if (process.env.NODE_ENV === "development") {console.log("FileTable permissions check:", {datasetPermissions,canEdit});}return canEdit;})()}/>
import { useKindeAuth } from "@kinde-oss/kinde-auth-react";import React from "react";import { Outlet } from "react-router-dom";import { RouteBreadcrumb } from "./RouteBreadcrumb";import UserProfile from "./UserProfile";export const AuthorisedLayout: React.FC = () => {const { isAuthenticated, isLoading } = useKindeAuth();if (isLoading) {return <p>Loading</p>;}if (!isAuthenticated) {return null;}return (<div className="container mx-auto px-5 space-y-4"><UserProfile /><RouteBreadcrumb /><Outlet /></div>);};
}},"node_modules/react-router": {"version": "7.6.3","resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.3.tgz","integrity": "sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==","license": "MIT","dependencies": {"cookie": "^1.0.1","set-cookie-parser": "^2.6.0"},"engines": {"node": ">=20.0.0"},"peerDependencies": {"react": ">=18","react-dom": ">=18"},"peerDependenciesMeta": {"react-dom": {"optional": true}}},"node_modules/react-router-dom": {"version": "7.6.3","resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.3.tgz","integrity": "sha512-DiWJm9qdUAmiJrVWaeJdu4TKu13+iB/8IEi0EW/XgaHCjW/vWGrwzup0GVvaMteuZjKnh5bEvJP/K0MDnzawHw==","license": "MIT","dependencies": {"react-router": "7.6.3"},"engines": {"node": ">=20.0.0"},"peerDependencies": {"react": ">=18","react-dom": ">=18"
"node_modules/react-router/node_modules/cookie": {"version": "1.0.2","resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz","integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==","license": "MIT","engines": {"node": ">=18"}},