Z3XA4BHNMZB6I34LZ626NQRUPHARWJ5UYHGT65IFQVSQ5B2TY6OQC OP2N5NWO6KOLP7X6ZAIYUAPTLMV2PAK5XPOBA7CHEB6K7IZ4OQIAC E7L6ECZK5DLB27CDJSB3USBUKU4E2HE4GDNUIQJB6HNU4J65MRMAC UQZD6FFK2L7ZZQGBQZRV4UDTFZ7QHNRFESMUZRH4XK3HBBSGA6LAC E3WSKRJTPPRD6ZEEURTO77N6GTHYJY7ISWQL2LN2VCCXA4YRH2CAC M3JUJ2WWZGCVMBITKRM5FUJMHFYL2QRMXJUVRUE4AC2RF74AOL5AC ROQGXQWL2V363K3W7TVVYKIAX4N4IWRERN5BJ7NYJRRVB6OMIJ4QC 4FBIL6IZUDNCXTM6EUHTEOJRHVI4LIIX4BU2IXPXKR362GKIAJMQC PVQBFR72OCQGYF2G2KDWNKBHWJ24N6D653X6KARBGUSYBIHIXPRQC LYPSC7BOH6T45FCPRHSCXILAJSJ74D5WSQTUIKPWD5ECXOYGUY5AC HBM7XFBGVMKW3P3VZFNQTJMINZ4DC3D4TZMT4TPTJRXC62HKZMMQC J2RLNDEXTGAV4BB6ANIIR7XJLJBHSB4NFQWSBWHNAFB6DMLGS5RAC 4RBE543WLHA7PIYT4W7YEJPF6XKZ2UGKPJBQ3CTLJ44AOMGHCEYQC KGUU3ZMRY65ZN5G6QC7P7ORPAXXTIMTDBJ37IC6AOKMGQJQ7FVMQC POIBWSL3JFHT2KN3STFSJX3INSYKEJTX6KSW3N7BVEKWX2GJ6T7QC 4M3EBLTLSS2BRCM42ZP7WVD4YMRRLGV2P2XF47IAV5XHHJD52HTQC import { useKindeAuth } from "@kinde-oss/kinde-auth-react";import React, { useEffect, useState } from "react";import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table";import {File,FilesResponse,NightFilter,PaginationMetadata,SpeciesOption} from "../types";// Define a component for just the filter controlsexport const FilesFilter: React.FC<{clusterId: string;onFilterChange: (filter: NightFilter) => void;currentFilter: NightFilter;onSpeciesFilterChange: (speciesId: string | null) => void;currentSpeciesId: string | null;speciesOptions: SpeciesOption[];totalFiles?: number;}> = ({currentFilter,onFilterChange,currentSpeciesId,onSpeciesFilterChange,speciesOptions,totalFiles}) => {return (<div className="flex items-center"><selectid="nightFilter"value={currentFilter}onChange={(e) => onFilterChange(e.target.value as NightFilter)}className="rounded-md border border-gray-300 bg-white py-1 px-3 text-sm shadow-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"><option value="solarNight">Solar night</option><option value="solarDay">Solar day</option><option value="civilNight">Civil night</option><option value="civilDay">Civil day</option></select></div>{speciesOptions.length > 0 && (<div className="flex items-center"><selectid="speciesFilter"value={currentSpeciesId || ''}onChange={(e) => onSpeciesFilterChange(e.target.value || null)}className="rounded-md border border-gray-300 bg-white py-1 px-3 text-sm shadow-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"><option value="">No Species Filter</option>{speciesOptions.map(species => (<option key={species.id} value={species.id}>{species.label}</option>))}</select></div>)}{totalFiles !== undefined && (<div className="text-gray-600 text-sm">{totalFiles} files</div>)}</div>);};interface FilesProps {clusterId: string;clusterName: string;datasetId: string | null; // Add datasetId to fetch species for the dataset (can be null)hideHeaderInfo?: boolean;nightFilter?: NightFilter;}const Files: React.FC<FilesProps> = ({clusterId,hideHeaderInfo = false,nightFilter: externalNightFilter}) => {// Force usage of parameters by logging their values in developmentReact.useEffect(() => {if (process.env.NODE_ENV === 'development') {}}, [clusterId, clusterName, datasetId, hideHeaderInfo]);console.log('Files component props:', { clusterId, clusterName, datasetId, hideHeaderInfo });const { isAuthenticated, isLoading: authLoading, getAccessToken } = useKindeAuth();const [files, setFiles] = useState<File[]>([]);const [pagination, setPagination] = useState<PaginationMetadata | null>(null);const [loading, setLoading] = useState<boolean>(true);const [error, setError] = useState<string | null>(null);const [currentPage, setCurrentPage] = useState<number>(1);const [hasMetadata, setHasMetadata] = useState<boolean>(false);const [hasMothMetadata, setHasMothMetadata] = useState<boolean>(false);const [nightFilter, setNightFilter] = useState<NightFilter>(externalNightFilter || 'none');const [speciesFilter, setSpeciesFilter] = useState<string | null>(null);const [speciesOptions, setSpeciesOptions] = useState<SpeciesOption[]>([]);const [hasSpecies, setHasSpecies] = useState<boolean>(false);// We track loading state but don't need to display it since we're showing species in the table, not as a filter headerconst [, setLoadingSpecies] = useState<boolean>(true);const formatDuration = (durationSec: number): string => {const minutes = Math.floor(durationSec / 60);const seconds = Math.floor(durationSec % 60);return `${minutes}:${seconds.toString().padStart(2, '0')}`;};try {} catch {}};};const handlePageChange = (newPage: number) => {if (newPage >= 1 && (!pagination || newPage <= pagination.totalPages)) {setCurrentPage(newPage);}}}}return (<div className="card p-6 bg-white shadow-sm rounded-lg">{error && (<div className="p-4 bg-red-50 text-red-700 rounded-md mb-4"><p className="font-medium">Error loading files</p><p className="text-sm mt-1">{error}</p></div>)}<><div className="w-full overflow-visible"><Table><TableHeader className="bg-muted"><TableRow className="border-b-2 border-primary/20"><TableHead className="py-3 font-bold text-sm uppercase">Duration</TableHead>{hasMetadata && (<TableHead className="py-3 font-bold text-sm uppercase">Metadata</TableHead>)}</TableRow></TableHeader><TableBody>}</TableRow></TableBody></Table></div>)}</>)}<div className="p-4 bg-yellow-50 text-yellow-800 rounded-md"><p className="font-medium">No files found for this cluster</p></div>)}</div>);};export default Files;{/* Initial state - completely empty cluster */}{!loading && !error && files.length === 0 && !pagination && speciesOptions.length === 0 && ({pagination && pagination.totalPages > 1 && files.length > 0 && (<PaginationcurrentPage={currentPage}totalPages={pagination.totalPages}onPageChange={handlePageChange}/>)}})()) : "—"}</TableCell>)}</TableRow>))) : (<TableRow><TableCellcolSpan={// Calculate total columns based on optional columns3 + // File, Duration, Moon Phase (always present)(hasSpecies ? 1 : 0) +(hasMothMetadata ? 3 : 0) +(hasMetadata ? 1 : 0)}className="text-center py-8">Try adjusting your filters to see more results</div></TableCell><div className="text-gray-500">// Format as key-value pairsif (typeof metaObj === 'object' && metaObj !== null) {const pairs = Object.entries(metaObj).map(([key, value]) => {// Handle arrays or objects as valueslet displayValue = value;if (Array.isArray(value)) {// Remove quotes and brackets from array stringdisplayValue = value.join(', ').replace(/[[\]"]*/g, '');} else if (typeof value === 'string') {// Remove quotes from string valuesdisplayValue = value.replace(/^"|"$/g, '');}return `${key}: ${displayValue}`;});return pairs.join(', ');}return JSON.stringify(metaObj);} catch (e) {console.error("Error formatting metadata:", e);return typeof file.metadata === 'string'? (file.metadata as string).substring(0, 50): "Invalid metadata";<TableCell className="whitespace-normal break-words"></TableCell>)}<TableCell className="whitespace-normal break-words">{formatDuration(Number(file.duration))}</TableCell>{hasMothMetadata && (<><TableCell className="whitespace-normal break-words">{file.mothMetadata?.gain || "—"}</TableCell><TableCell className="whitespace-normal break-words">{formatBatteryVoltage(file.mothMetadata?.batteryV)}</TableCell><TableCell className="whitespace-normal break-words">{formatTemperature(file.mothMetadata?.tempC)}</TableCell></>)}<TableCell className="whitespace-normal break-words">{formatMoonPhase(file.moonPhase)}</TableCell>{hasMetadata && (<TableCell className="whitespace-normal break-words">{file.metadata ? ((() => {try {// Parse metadata if it's a stringconst metaObj = typeof file.metadata === 'string'? JSON.parse(file.metadata): file.metadata;{file.species && file.species.length > 0 ? (<div className="flex flex-wrap gap-1">{file.species.map(species => (<divkey={species.id}className="inline-block px-3 py-1 rounded-full text-xs font-medium bg-stone-200 text-stone-800 text-center whitespace-nowrap overflow-hidden text-ellipsis"style={{ minWidth: '80px', maxWidth: '150px' }}title={species.label} // Show full name on hover>{species.label}</div>))}</div>) : "—"}{files.length > 0 ? (files.map((file) => (<TableRow key={file.id}><TableCell className="font-medium whitespace-normal break-words">{file.fileName}</TableCell>{hasSpecies && ({hasMothMetadata && (<><TableHead className="py-3 font-bold text-sm uppercase">Gain</TableHead><TableHead className="py-3 font-bold text-sm uppercase">Battery</TableHead><TableHead className="py-3 font-bold text-sm uppercase">Temp</TableHead></>)}<TableHead className="py-3 font-bold text-sm uppercase">Moon Phase</TableHead><TableHead className="w-[240px] py-3 font-bold text-sm uppercase">File</TableHead>{hasSpecies && (<TableHead className="py-3 font-bold text-sm uppercase">Species</TableHead>)}{!loading && !error && ({loading && (<div className="py-4 text-center text-gray-500 animate-fade-in">Loading files...</div>)}{/* Filter controls - always show filters when we have data to filter, even if current filters result in no files */}{!loading && !error && (pagination || speciesOptions.length > 0) && (<div className="mb-4"><FilesFilterclusterId={clusterId}currentFilter={nightFilter}onFilterChange={setNightFilter}currentSpeciesId={speciesFilter}onSpeciesFilterChange={handleSpeciesFilterChange}speciesOptions={speciesOptions}totalFiles={pagination?.totalItems}/></div>)}// Effect to handle clusterId changes and initial loaduseEffect(() => {// Reset state when cluster changessetFiles([]);setPagination(null);setLoading(true);setError(null);// Use a small delay before fetching to allow the UI to show loading state// This prevents the brief flash of empty contentconst timeoutId = setTimeout(() => {if (isAuthenticated && !authLoading) {fetchFiles();}}, 50);return () => clearTimeout(timeoutId);}, [isAuthenticated, authLoading, clusterId, fetchFiles]);// Update internal state when external filter changes// This doesn't trigger a fetch because we use 'setNightFilter' which// will be caught by fetchFiles dependencyuseEffect(() => {if (externalNightFilter !== undefined) {setNightFilter(externalNightFilter);}}, [externalNightFilter]);// Fetch species for the dataset to populate the species filter dropdownuseEffect(() => {setLoadingSpecies(true);setSpeciesOptions([]);const fetchSpecies = async () => {if (!isAuthenticated || !datasetId) {setLoadingSpecies(false);setSpeciesOptions([]);return;}try {const accessToken = await getAccessToken();const url = `/api/species?datasetId=${encodeURIComponent(datasetId)}`;const response = await fetch(url, {headers: {Authorization: `Bearer ${accessToken}`,},});if (!response.ok) {throw new Error(`Server error: ${response.status}`);}const data = await response.json() as { data: SpeciesOption[] };if (!data.data || !Array.isArray(data.data)) {throw new Error("Invalid response format");}setSpeciesOptions(data.data);setHasSpecies(data.data.length > 0);} catch (err) {console.error("Error fetching species:", err);// Don't show error for species, just hide the filtersetSpeciesOptions([]);} finally {setLoadingSpecies(false);}};if (isAuthenticated && !authLoading) {fetchSpecies();}}, [isAuthenticated, authLoading, getAccessToken, datasetId]);// Effect to handle pagination and filter changesuseEffect(() => {// Skip the initial render caused by the dependency array initialization// Only fetch if we're not already in the initial loading state (first load)if (!loading && (currentPage > 1 || nightFilter !== 'none' || speciesFilter !== null)) {// Use skipLoadingState=true to avoid flashing when changing filters or pagination// This allows data to be replaced smoothly without showing loading indicatorsfetchFiles(true);}}, [currentPage, nightFilter, speciesFilter, fetchFiles, loading]);}, [isAuthenticated, authLoading, getAccessToken, clusterId, currentPage, nightFilter, speciesFilter, speciesOptions.length]);});const validMothMetadata = data.data.some(file =>file.mothMetadata &&(file.mothMetadata.gain !== null ||file.mothMetadata.batteryV !== null ||file.mothMetadata.tempC !== null));const validSpeciesData = data.data.some(file =>file.species && file.species.length > 0);// Use React 18's batch updates to minimize re-renders// All of these state updates will be batched into a single rendersetHasMetadata(validMetadata);setHasMothMetadata(validMothMetadata);setHasSpecies(validSpeciesData || speciesOptions.length > 0);setFiles(data.data);setPagination(data.pagination);setError(null);} catch (err) {const errorMessage = err instanceof Error ? err.message : "Failed to fetch files";setError(errorMessage);// Clear data when there's an errorsetFiles([]);setPagination(null);} finally {setLoading(false);if (!data.data || !Array.isArray(data.data) || !data.pagination) {throw new Error("Invalid response format");}// Process all metadata checks and state updates in a batch// to reduce multiple re-rendersconst validMetadata = data.data.some(file => {if (!file.metadata) return false;try {const meta = typeof file.metadata === 'string'? JSON.parse(file.metadata): file.metadata;return meta && typeof meta === 'object' && Object.keys(meta).length > 0;} catch {return false;if (!response.ok) {throw new Error(`Server error: ${response.status}`);}const data = await response.json() as FilesResponse;const response = await fetch(url, {headers: {Authorization: `Bearer ${accessToken}`,},});// Build URL with filterslet url = `/api/files?clusterId=${encodeURIComponent(clusterId)}&page=${currentPage}&pageSize=100`;// Add night filtersswitch (nightFilter) {case 'solarNight':url += '&solarNight=true';break;case 'solarDay':url += '&solarNight=false';break;case 'civilNight':url += '&civilNight=true';break;case 'civilDay':url += '&civilNight=false';break;// 'none' doesn't add any filter parameters}// Add species filter if selectedif (speciesFilter) {url += `&speciesId=${encodeURIComponent(speciesFilter)}`;}try {const accessToken = await getAccessToken();if (!skipLoadingState) {setLoading(true);}return;}const fetchFiles = React.useCallback(async (skipLoadingState = false) => {if (!isAuthenticated || !clusterId) {if (!authLoading) {setLoading(false);};const handleSpeciesFilterChange = (newSpeciesId: string | null) => {setSpeciesFilter(newSpeciesId);// Reset to first page when changing filterssetCurrentPage(1);};// Removed handleFilterChange function since we now use external filter};try {} catch {return "—";}return Number(temp).toFixed(1) + '°C';const formatTemperature = (temp: number | null | undefined): string => {if (temp === null || temp === undefined) return "—";const formatBatteryVoltage = (volts: number | null | undefined): string => {if (volts === null || volts === undefined) return "—";try {return Number(volts).toFixed(1) + 'V';} catch {return "—";}return "—";return Number(phase).toFixed(2);// Keep these formatters commented out for future use// const formatDateTime = (isoString: string): string => {// try {// const date = new Date(isoString);// return new Intl.DateTimeFormat('en-GB', {// year: 'numeric',// month: 'short',// day: '2-digit',// hour: '2-digit',// minute: '2-digit',// hour12: false// }).format(date);// } catch {// return isoString;// }// };// const formatSampleRate = (rate: number): string => {// return `${(rate / 1000).toFixed(1)} kHz`;// };const formatMoonPhase = (phase: number | null | undefined): string => {if (phase === null || phase === undefined) return "—";clusterName,datasetId,<option value="none">No Time Filter</option><div className="flex flex-wrap items-center gap-4">import { Pagination } from "./ui/pagination";
import { useKindeAuth } from "@kinde-oss/kinde-auth-react";import React, { useEffect, useState } from "react";import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table";import { Pagination } from "./ui/pagination";import {File,FilesResponse,NightFilter,PaginationMetadata,SpeciesOption} from "../types";// Define a component for just the filter controlsexport const ClusterFilter: React.FC<{clusterId: string;onFilterChange: (filter: NightFilter) => void;currentFilter: NightFilter;onSpeciesFilterChange: (speciesId: string | null) => void;currentSpeciesId: string | null;speciesOptions: SpeciesOption[];totalFiles?: number;}> = ({currentFilter,onFilterChange,currentSpeciesId,onSpeciesFilterChange,speciesOptions,totalFiles}) => {return (<div className="flex flex-wrap items-center gap-4"><div className="flex items-center"><selectid="nightFilter"value={currentFilter}onChange={(e) => onFilterChange(e.target.value as NightFilter)}className="rounded-md border border-gray-300 bg-white py-1 px-3 text-sm shadow-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"><option value="none">No Time Filter</option><option value="solarNight">Solar night</option><option value="solarDay">Solar day</option><option value="civilNight">Civil night</option><option value="civilDay">Civil day</option></select></div>{speciesOptions.length > 0 && (<div className="flex items-center"><selectid="speciesFilter"value={currentSpeciesId || ''}onChange={(e) => onSpeciesFilterChange(e.target.value || null)}className="rounded-md border border-gray-300 bg-white py-1 px-3 text-sm shadow-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"><option value="">No Species Filter</option>{speciesOptions.map(species => (<option key={species.id} value={species.id}>{species.label}</option>))}</select></div>)}{totalFiles !== undefined && (<div className="text-gray-600 text-sm">{totalFiles} files</div>)}</div>);};interface ClusterProps {clusterId: string;clusterName: string;datasetId: string | null; // Add datasetId to fetch species for the dataset (can be null)hideHeaderInfo?: boolean;nightFilter?: NightFilter;}const Cluster: React.FC<ClusterProps> = ({clusterId,clusterName,datasetId,hideHeaderInfo = false,nightFilter: externalNightFilter}) => {// Force usage of parameters by logging their values in developmentReact.useEffect(() => {if (process.env.NODE_ENV === 'development') {console.log('Cluster component props:', { clusterId, clusterName, datasetId, hideHeaderInfo });}}, [clusterId, clusterName, datasetId, hideHeaderInfo]);const { isAuthenticated, isLoading: authLoading, getAccessToken } = useKindeAuth();const [files, setFiles] = useState<File[]>([]);const [pagination, setPagination] = useState<PaginationMetadata | null>(null);const [loading, setLoading] = useState<boolean>(true);const [error, setError] = useState<string | null>(null);const [currentPage, setCurrentPage] = useState<number>(1);const [hasMetadata, setHasMetadata] = useState<boolean>(false);const [hasMothMetadata, setHasMothMetadata] = useState<boolean>(false);const [nightFilter, setNightFilter] = useState<NightFilter>(externalNightFilter || 'none');const [speciesFilter, setSpeciesFilter] = useState<string | null>(null);const [speciesOptions, setSpeciesOptions] = useState<SpeciesOption[]>([]);const [hasSpecies, setHasSpecies] = useState<boolean>(false);// We track loading state but don't need to display it since we're showing species in the table, not as a filter headerconst [, setLoadingSpecies] = useState<boolean>(true);const formatDuration = (durationSec: number): string => {const minutes = Math.floor(durationSec / 60);const seconds = Math.floor(durationSec % 60);return `${minutes}:${seconds.toString().padStart(2, '0')}`;};// Keep these formatters commented out for future use// const formatDateTime = (isoString: string): string => {// try {// const date = new Date(isoString);// return new Intl.DateTimeFormat('en-GB', {// year: 'numeric',// month: 'short',// day: '2-digit',// hour: '2-digit',// minute: '2-digit',// hour12: false// }).format(date);// } catch {// return isoString;// }// };// const formatSampleRate = (rate: number): string => {// return `${(rate / 1000).toFixed(1)} kHz`;// };const formatMoonPhase = (phase: number | null | undefined): string => {if (phase === null || phase === undefined) return "—";try {return Number(phase).toFixed(2);} catch {return "—";}};const formatBatteryVoltage = (volts: number | null | undefined): string => {if (volts === null || volts === undefined) return "—";try {return Number(volts).toFixed(1) + 'V';} catch {return "—";}};const formatTemperature = (temp: number | null | undefined): string => {if (temp === null || temp === undefined) return "—";try {return Number(temp).toFixed(1) + '°C';} catch {return "—";}};const handlePageChange = (newPage: number) => {if (newPage >= 1 && (!pagination || newPage <= pagination.totalPages)) {setCurrentPage(newPage);}};const handleSpeciesFilterChange = (newSpeciesId: string | null) => {setSpeciesFilter(newSpeciesId);// Reset to first page when changing filterssetCurrentPage(1);};// Removed handleFilterChange function since we now use external filterconst fetchFiles = React.useCallback(async (skipLoadingState = false) => {if (!isAuthenticated || !clusterId) {if (!authLoading) {setLoading(false);}return;}if (!skipLoadingState) {setLoading(true);}try {const accessToken = await getAccessToken();// Build URL with filterslet url = `/api/files?clusterId=${encodeURIComponent(clusterId)}&page=${currentPage}&pageSize=100`;// Add night filtersswitch (nightFilter) {case 'solarNight':url += '&solarNight=true';break;case 'solarDay':url += '&solarNight=false';break;case 'civilNight':url += '&civilNight=true';break;case 'civilDay':url += '&civilNight=false';break;// 'none' doesn't add any filter parameters}// Add species filter if selectedif (speciesFilter) {url += `&speciesId=${encodeURIComponent(speciesFilter)}`;}const response = await fetch(url, {headers: {Authorization: `Bearer ${accessToken}`,},});if (!response.ok) {throw new Error(`Server error: ${response.status}`);}const data = await response.json() as FilesResponse;if (!data.data || !Array.isArray(data.data) || !data.pagination) {throw new Error("Invalid response format");}// Process all metadata checks and state updates in a batch// to reduce multiple re-rendersconst validMetadata = data.data.some(file => {if (!file.metadata) return false;try {const meta = typeof file.metadata === 'string'? JSON.parse(file.metadata): file.metadata;return meta && typeof meta === 'object' && Object.keys(meta).length > 0;} catch {return false;}});const validMothMetadata = data.data.some(file =>file.mothMetadata &&(file.mothMetadata.gain !== null ||file.mothMetadata.batteryV !== null ||file.mothMetadata.tempC !== null));const validSpeciesData = data.data.some(file =>file.species && file.species.length > 0);// Use React 18's batch updates to minimize re-renders// All of these state updates will be batched into a single rendersetHasMetadata(validMetadata);setHasMothMetadata(validMothMetadata);setHasSpecies(validSpeciesData || speciesOptions.length > 0);setFiles(data.data);setPagination(data.pagination);setError(null);} catch (err) {const errorMessage = err instanceof Error ? err.message : "Failed to fetch files";setError(errorMessage);// Clear data when there's an errorsetFiles([]);setPagination(null);} finally {setLoading(false);}}, [isAuthenticated, authLoading, getAccessToken, clusterId, currentPage, nightFilter, speciesFilter, speciesOptions.length]);// Effect to handle clusterId changes and initial loaduseEffect(() => {// Reset state when cluster changessetFiles([]);setPagination(null);setLoading(true);setError(null);// Use a small delay before fetching to allow the UI to show loading state// This prevents the brief flash of empty contentconst timeoutId = setTimeout(() => {if (isAuthenticated && !authLoading) {fetchFiles();}}, 50);return () => clearTimeout(timeoutId);}, [isAuthenticated, authLoading, clusterId, fetchFiles]);// Update internal state when external filter changes// This doesn't trigger a fetch because we use 'setNightFilter' which// will be caught by fetchFiles dependencyuseEffect(() => {if (externalNightFilter !== undefined) {setNightFilter(externalNightFilter);}}, [externalNightFilter]);// Effect to handle pagination and filter changesuseEffect(() => {// Skip the initial render caused by the dependency array initialization// Only fetch if we're not already in the initial loading state (first load)if (!loading && (currentPage > 1 || nightFilter !== 'none' || speciesFilter !== null)) {// Use skipLoadingState=true to avoid flashing when changing filters or pagination// This allows data to be replaced smoothly without showing loading indicatorsfetchFiles(true);}}, [currentPage, nightFilter, speciesFilter, fetchFiles, loading]);// Fetch species for the dataset to populate the species filter dropdownuseEffect(() => {setLoadingSpecies(true);setSpeciesOptions([]);const fetchSpecies = async () => {if (!isAuthenticated || !datasetId) {setLoadingSpecies(false);setSpeciesOptions([]);return;}try {const accessToken = await getAccessToken();const url = `/api/species?datasetId=${encodeURIComponent(datasetId)}`;const response = await fetch(url, {headers: {Authorization: `Bearer ${accessToken}`,},});if (!response.ok) {throw new Error(`Server error: ${response.status}`);}const data = await response.json() as { data: SpeciesOption[] };if (!data.data || !Array.isArray(data.data)) {throw new Error("Invalid response format");}setSpeciesOptions(data.data);setHasSpecies(data.data.length > 0);} catch (err) {console.error("Error fetching species:", err);// Don't show error for species, just hide the filtersetSpeciesOptions([]);} finally {setLoadingSpecies(false);}};if (isAuthenticated && !authLoading) {fetchSpecies();}}, [isAuthenticated, authLoading, getAccessToken, datasetId]);return (<div className="card p-6 bg-white shadow-sm rounded-lg">{/* Filter controls - always show filters when we have data to filter, even if current filters result in no files */}{!loading && !error && (pagination || speciesOptions.length > 0) && (<div className="mb-4"><ClusterFilterclusterId={clusterId}currentFilter={nightFilter}onFilterChange={setNightFilter}currentSpeciesId={speciesFilter}onSpeciesFilterChange={handleSpeciesFilterChange}speciesOptions={speciesOptions}totalFiles={pagination?.totalItems}/></div>)}{loading && (<div className="py-4 text-center text-gray-500 animate-fade-in">Loading files...</div>)}{error && (<div className="p-4 bg-red-50 text-red-700 rounded-md mb-4"><p className="font-medium">Error loading files</p><p className="text-sm mt-1">{error}</p></div>)}{!loading && !error && (<><div className="w-full overflow-visible"><Table><TableHeader className="bg-muted"><TableRow className="border-b-2 border-primary/20"><TableHead className="w-[240px] py-3 font-bold text-sm uppercase">File</TableHead>{hasSpecies && (<TableHead className="py-3 font-bold text-sm uppercase">Species</TableHead>)}<TableHead className="py-3 font-bold text-sm uppercase">Duration</TableHead>{hasMothMetadata && (<><TableHead className="py-3 font-bold text-sm uppercase">Gain</TableHead><TableHead className="py-3 font-bold text-sm uppercase">Battery</TableHead><TableHead className="py-3 font-bold text-sm uppercase">Temp</TableHead></>)}<TableHead className="py-3 font-bold text-sm uppercase">Moon Phase</TableHead>{hasMetadata && (<TableHead className="py-3 font-bold text-sm uppercase">Metadata</TableHead>)}</TableRow></TableHeader><TableBody>{files.length > 0 ? (files.map((file) => (<TableRow key={file.id}><TableCell className="font-medium whitespace-normal break-words">{file.fileName}</TableCell>{hasSpecies && (<TableCell className="whitespace-normal break-words">{file.species && file.species.length > 0 ? (<div className="flex flex-wrap gap-1">{file.species.map(species => (<divkey={species.id}className="inline-block px-3 py-1 rounded-full text-xs font-medium bg-stone-200 text-stone-800 text-center whitespace-nowrap overflow-hidden text-ellipsis"style={{ minWidth: '80px', maxWidth: '150px' }}title={species.label} // Show full name on hover>{species.label}</div>))}</div>) : "—"}</TableCell>)}<TableCell className="whitespace-normal break-words">{formatDuration(Number(file.duration))}</TableCell>{hasMothMetadata && (<><TableCell className="whitespace-normal break-words">{file.mothMetadata?.gain || "—"}</TableCell><TableCell className="whitespace-normal break-words">{formatBatteryVoltage(file.mothMetadata?.batteryV)}</TableCell><TableCell className="whitespace-normal break-words">{formatTemperature(file.mothMetadata?.tempC)}</TableCell></>)}<TableCell className="whitespace-normal break-words">{formatMoonPhase(file.moonPhase)}</TableCell>{hasMetadata && (<TableCell className="whitespace-normal break-words">{file.metadata ? ((() => {try {// Parse metadata if it's a stringconst metaObj = typeof file.metadata === 'string'? JSON.parse(file.metadata): file.metadata;// Format as key-value pairsif (typeof metaObj === 'object' && metaObj !== null) {const pairs = Object.entries(metaObj).map(([key, value]) => {// Handle arrays or objects as valueslet displayValue = value;if (Array.isArray(value)) {// Remove quotes and brackets from array stringdisplayValue = value.join(', ').replace(/[[\]"]*/g, '');} else if (typeof value === 'string') {// Remove quotes from string valuesdisplayValue = value.replace(/^"|"$/g, '');}return `${key}: ${displayValue}`;});return pairs.join(', ');}return JSON.stringify(metaObj);} catch (e) {console.error("Error formatting metadata:", e);return typeof file.metadata === 'string'? (file.metadata as string).substring(0, 50): "Invalid metadata";}})()) : "—"}</TableCell>)}</TableRow>))) : (<TableRow><TableCellcolSpan={// Calculate total columns based on optional columns3 + // File, Duration, Moon Phase (always present)(hasSpecies ? 1 : 0) +(hasMothMetadata ? 3 : 0) +(hasMetadata ? 1 : 0)}className="text-center py-8"><div className="text-gray-500">Try adjusting your filters to see more results</div></TableCell></TableRow>)}</TableBody></Table></div>{pagination && pagination.totalPages > 1 && files.length > 0 && (<PaginationcurrentPage={currentPage}totalPages={pagination.totalPages}onPageChange={handlePageChange}/>)}</>)}{/* Initial state - completely empty cluster */}{!loading && !error && files.length === 0 && !pagination && speciesOptions.length === 0 && (<div className="p-4 bg-yellow-50 text-yellow-800 rounded-md"><p className="font-medium">No files found for this cluster</p></div>)}</div>);};export default Cluster;
{/* Filters moved to breadcrumb line - no species filter here since we're using it in Files component */}{/* Removing this filter component since we now have it integrated into the Files component */}
{/* Filters moved to breadcrumb line - no species filter here since we're using it in Cluster component */}{/* Removing this filter component since we now have it integrated into the Cluster component */}