6W73J6WJQHALRF4ZLBJ4JH6H7UYT74KFPKFH537T2BRW2G6MCLRQC
VFENVAHOAERPOWJFWVQBS5GXK3F422I62ZGQGOJHALKZZK2Q2SJAC
NK2VPE6EAGQINWN6SIELRLOR3K7KTA3MSQLLDGKFVWU35LSB37RAC
ONSQYCF6NFUEA24ORB62W4P62LKUMV7C5PLYRZQULHFDNROEY2HQC
OBXY6BHNROIV7W4O3MMQ6GTQXU2XUKF5A4DCJYDLUEUHV72E7BNAC
4FBIL6IZUDNCXTM6EUHTEOJRHVI4LIIX4BU2IXPXKR362GKIAJMQC
DBDM3MJDJ5TRAH3FA7MLBGFUPZF4GIM4CPZAKFNS5MV3UYQOAFNQC
LSAQ6ZM2NELU3FIWKEFBOXKVLSZS2ZOK2PHPHJRWPVZ5CVILSUYQC
description: item.description
});
description: item.description,
callTypes: []
};
acc[item.fileId].push(existingSpecies);
}
// Add call type if present and not already added
if (item.callTypeId && item.callTypeName) {
const existingCallType = existingSpecies.callTypes.find(ct => ct.id === item.callTypeId);
if (!existingCallType) {
existingSpecies.callTypes.push({
id: item.callTypeId,
name: item.callTypeName
});
}
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table";
import { Pagination } from "./ui/pagination";
import React, { useEffect, useState } from "react";
import { File as FileType, FilesResponse, NightFilter, PaginationMetadata, SpeciesOption } from "../types";
'bg-stone-200 text-stone-800',
'bg-gray-200 text-gray-800',
'bg-slate-200 text-slate-800',
'bg-zinc-200 text-zinc-800',
'bg-neutral-200 text-neutral-800',
'bg-stone-300 text-stone-800',
'bg-gray-300 text-gray-800',
'bg-slate-300 text-slate-800'
"bg-stone-200 text-stone-800",
"bg-gray-200 text-gray-800",
"bg-slate-200 text-slate-800",
"bg-zinc-200 text-zinc-800",
"bg-neutral-200 text-neutral-800",
"bg-stone-300 text-stone-800",
"bg-gray-300 text-gray-800",
"bg-slate-300 text-slate-800",
'bg-stone-200 text-stone-800',
'bg-gray-200 text-gray-800',
'bg-slate-200 text-slate-800',
'bg-zinc-200 text-zinc-800',
'bg-neutral-200 text-neutral-800',
'bg-stone-300 text-stone-800',
'bg-gray-300 text-gray-800',
'bg-slate-300 text-slate-800'
"bg-stone-200 text-stone-800",
"bg-gray-200 text-gray-800",
"bg-slate-200 text-slate-800",
"bg-zinc-200 text-zinc-800",
"bg-neutral-200 text-neutral-800",
"bg-stone-300 text-stone-800",
"bg-gray-300 text-gray-800",
"bg-slate-300 text-slate-800",
// Generate different gray shades for call types (using darker grays to distinguish from species)
const getCallTypeColor = (callTypeId: string): string => {
const grayShades = [
"bg-stone-200 text-stone-800",
"bg-gray-200 text-gray-800",
"bg-slate-200 text-slate-800",
"bg-zinc-200 text-zinc-800",
"bg-neutral-200 text-neutral-800",
"bg-stone-300 text-stone-800",
"bg-gray-300 text-gray-800",
"bg-slate-300 text-slate-800",
];
// Simple hash function to get consistent shade for each call type
let hash = 0;
for (let i = 0; i < callTypeId.length; i++) {
hash = ((hash << 5) - hash + callTypeId.charCodeAt(i)) & 0xffffffff;
}
return grayShades[Math.abs(hash) % grayShades.length];
};
let url = '';
if (apiEndpoint === 'files') {
url = `/api/files?clusterId=${encodeURIComponent(requiredParams.clusterId!)}&page=${currentPage}&pageSize=100`;
let url = "";
if (apiEndpoint === "files") {
url = `/api/files?clusterId=${
encodeURIComponent(requiredParams.clusterId!)
}&page=${currentPage}&pageSize=100`;
url = `/api/selection?datasetId=${encodeURIComponent(requiredParams.datasetId!)}&speciesId=${encodeURIComponent(speciesFilter!)}&page=${currentPage}&pageSize=100`;
url = `/api/selection?datasetId=${encodeURIComponent(requiredParams.datasetId!)}&speciesId=${
encodeURIComponent(speciesFilter!)
}&page=${currentPage}&pageSize=100`;
const validMothMetadata = data.data.some(file =>
file.mothMetadata &&
(file.mothMetadata.gain !== null ||
file.mothMetadata.batteryV !== null ||
file.mothMetadata.tempC !== null)
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
const validSpeciesData = data.data.some(file => file.species && file.species.length > 0);
// Check if any files have call types
const validCallTypesData = data.data.some(file =>
file.species && file.species.some(species => species.callTypes && species.callTypes.length > 0)
}, [isAuthenticated, authLoading, getAccessToken, requiredParams.clusterId, requiredParams.datasetId, speciesFilter, currentPage, nightFilter, speciesOptions.length, apiEndpoint]);
}, [
isAuthenticated,
authLoading,
getAccessToken,
requiredParams.clusterId,
requiredParams.datasetId,
speciesFilter,
currentPage,
nightFilter,
speciesOptions.length,
apiEndpoint,
]);
{hasSpecies && (
<TableHead className="py-3 font-bold text-sm uppercase">Species</TableHead>
)}
{hasSpecies && <TableHead className="py-3 font-bold text-sm uppercase">Species</TableHead>}
{hasCallTypes && <TableHead className="py-3 font-bold text-sm uppercase">Call Types</TableHead>}
{files.length > 0 ? (
files.map((file) => (
<TableRow key={file.id} className={`hover:bg-primary/5 ${file.upload ? "bg-gray-50" : ""}`}>
<TableCell
className="font-medium whitespace-normal break-words cursor-pointer"
onClick={() => handleFileClick(file)}
>
{file.fileName}
</TableCell>
<TableCell
className="whitespace-normal break-words cursor-pointer"
onClick={() => handleFileClick(file)}
>
{file.upload ? (
<Check className="h-4 w-4 text-gray-600" />
) : (
<X className="h-4 w-4 text-gray-600" />
)}
</TableCell>
{hasSpecies && (
<TableCell
{files.length > 0
? (
files.map((file) => (
<TableRow key={file.id} className={`hover:bg-primary/5 ${file.upload ? "bg-gray-50" : ""}`}>
<TableCell
className="font-medium whitespace-normal break-words cursor-pointer"
onClick={() =>
handleFileClick(file)}
>
{file.fileName}
</TableCell>
<TableCell
{file.species && file.species.length > 0 ? (
<div className="flex flex-wrap gap-1">
{file.species.map(species => (
<div
key={species.id}
className={`inline-block px-3 py-1 rounded-full text-xs font-medium ${getSpeciesColor(species.id)} text-center whitespace-nowrap overflow-hidden text-ellipsis`}
style={{ minWidth: '80px', maxWidth: '150px' }}
title={species.label}
>
{species.label}
</div>
))}
</div>
) : "—"}
{file.upload
? <Check className="h-4 w-4 text-gray-600" />
: <X className="h-4 w-4 text-gray-600" />}
{file.mothMetadata?.gain || "—"}
{file.species && file.species.length > 0
? (
<div className="flex flex-wrap gap-1">
{file.species.map(species => (
<div
key={species.id}
className={`inline-block px-3 py-1 rounded-full text-xs font-medium ${
getSpeciesColor(species.id)
} text-center whitespace-nowrap overflow-hidden text-ellipsis`}
style={{ minWidth: "80px", maxWidth: "150px" }}
title={species.label}
>
{species.label}
</div>
))}
</div>
)
: "—"}
{formatBatteryVoltage(file.mothMetadata?.batteryV)}
{file.species && file.species.some(species =>
species.callTypes && species.callTypes.length > 0
)
? (
<div className="flex flex-wrap gap-1">
{file.species.flatMap(species =>
species.callTypes?.map(callType => (
<div
key={`${species.id}-${callType.id}`}
className={`inline-block px-3 py-1 rounded-full text-xs font-medium ${
getCallTypeColor(callType.id)
} text-center whitespace-nowrap overflow-hidden text-ellipsis`}
style={{ minWidth: "80px", maxWidth: "150px" }}
title={callType.name}
>
{callType.name}
</div>
)) || []
)}
</div>
)
: "—"}
<TableCell
)}
<TableCell
className="whitespace-normal break-words cursor-pointer"
onClick={() => handleFileClick(file)}
>
{formatDuration(Number(file.duration))}
</TableCell>
{hasMothMetadata && (
<>
<TableCell
className="whitespace-normal break-words cursor-pointer"
onClick={() => handleFileClick(file)}
>
{file.mothMetadata?.gain || "—"}
</TableCell>
<TableCell
className="whitespace-normal break-words cursor-pointer"
onClick={() => handleFileClick(file)}
>
{formatBatteryVoltage(file.mothMetadata?.batteryV)}
</TableCell>
<TableCell
className="whitespace-normal break-words cursor-pointer"
onClick={() => handleFileClick(file)}
>
{formatTemperature(file.mothMetadata?.tempC)}
</TableCell>
</>
)}
<TableCell
className="whitespace-normal break-words cursor-pointer"
onClick={() => handleFileClick(file)}
>
{formatMoonPhase(file.moonPhase)}
</TableCell>
{hasMetadata && (
<TableCell
</>
)}
<TableCell
className="whitespace-normal break-words cursor-pointer"
onClick={() => handleFileClick(file)}
)}
</TableRow>
))
)
: (
<TableRow>
<TableCell
colSpan={
// Calculate total columns based on optional columns
4 // File, Status, Duration, Moon Phase (always present)
+ (hasSpecies ? 1 : 0)
+ (hasCallTypes ? 1 : 0)
+ (hasMothMetadata ? 3 : 0)
+ (hasMetadata ? 1 : 0)
}
className="text-center py-8"
))
) : (
<TableRow>
<TableCell
colSpan={
// Calculate total columns based on optional columns
4 + // File, Status, Duration, Moon Phase (always present)
(hasSpecies ? 1 : 0) +
(hasMothMetadata ? 3 : 0) +
(hasMetadata ? 1 : 0)
}
className="text-center py-8"
>
<div className="text-gray-500">
{apiEndpoint === 'selection' ? "No files found with the selected filters" : "Try adjusting your filters to see more results"}
</div>
</TableCell>
</TableRow>
)}
)}
{!loading && !error && files.length === 0 && !pagination && speciesOptions.length === 0 && apiEndpoint === 'files' && (
<div className="p-4 bg-yellow-50 text-yellow-800 rounded-md">
<p className="font-medium">No files found for this cluster</p>
</div>
)}
{!loading && !error && files.length === 0 && !pagination && speciesOptions.length === 0 && apiEndpoint === "files"
&& (
<div className="p-4 bg-yellow-50 text-yellow-800 rounded-md">
<p className="font-medium">No files found for this cluster</p>
</div>
)}