diff --git a/ui/src/components/Bouts/BoutItem.tsx b/ui/src/components/Bouts/BoutItem.tsx index 655aedb4a..f3caaf722 100644 --- a/ui/src/components/Bouts/BoutItem.tsx +++ b/ui/src/components/Bouts/BoutItem.tsx @@ -13,6 +13,7 @@ import { Feed, useDetectionsCountQuery, } from "@/graphql/generated"; +import { durationString } from "@/utils/time"; import CategoryIcon from "./CategoryIcon"; @@ -145,16 +146,3 @@ export default function BoutItem({ ); } - -function durationString(durationMs: number | null | undefined) { - if (typeof durationMs !== "number") return ""; - const duration = intervalToDuration({ start: 0, end: durationMs }); - const hours = Math.floor(durationMs / 1000 / 60 / 60); - - const zeroPad = (num: number) => String(num).padStart(2, "0"); - const formatted = [hours, duration.minutes ?? 0, duration.seconds ?? 0] - .map(zeroPad) - .join(":"); - - return formatted; -} diff --git a/ui/src/components/DetectionsTable.tsx b/ui/src/components/DetectionsTable.tsx index 609276daa..9faa3bbfc 100644 --- a/ui/src/components/DetectionsTable.tsx +++ b/ui/src/components/DetectionsTable.tsx @@ -28,6 +28,7 @@ import { useNotifyConfirmedCandidateMutation, useSetDetectionVisibleMutation, } from "@/graphql/generated"; +import { ReportBoutList } from "@/modules/reports/ReportBoutList"; import { analytics } from "@/utils/analytics"; import { formatTimestamp } from "@/utils/time"; @@ -53,8 +54,11 @@ export default function DetectionsTable({ | "sourceIp" | "description" >[]; - feed: Pick; - candidate: Pick; + feed: Pick; + candidate: Pick< + Candidate, + "id" | "visible" | "minTime" | "maxTime" | "category" + >; onDetectionUpdate: () => void; }) { const offsetPadding = 15; @@ -182,6 +186,15 @@ export default function DetectionsTable({ + {candidate?.category && ( + + )} + {currentUser?.moderator && ( ; + +export function ReportBoutList({ + feed, + minTime, + maxTime, + category, +}: { + feed: Pick; + minTime: Date; + maxTime: Date; + category: DetectionCategory; +}) { + console.log("feed id?", feed.id); + const { data: boutData, isLoading } = useBoutsQuery({ + feedId: feed.id, + filter: { + category: { eq: detectionCategoryToAudioCategory(category) }, + startTime: { lessThanOrEqual: maxTime }, + endTime: { greaterThanOrEqual: minTime }, + }, + }); + + const bouts = boutData?.bouts; + + return ( + +

Bouts

+ + + + # + ID + Name + Category + Start time + End time + Duration + Actions + + + + {isLoading && ( + + {_.range(8).map((_, idx) => ( + + + + ))} + + )} + {!isLoading && + (bouts?.count || 0) > 0 && + bouts?.results?.map((bout, idx) => ( + + ))} + {!isLoading && (bouts?.count || 0) === 0 && ( + + + No bouts for this candidate + + + )} + +
+
+ ); +} + +function BoutItem({ + bout, + feed, + index, +}: { + bout: BoutResult; + feed: Pick; + index: number; +}) { + const [duration, setDuration] = useState( + bout.duration && bout.duration * 1000, + ); + const isLive = bout.endTime === undefined || bout.endTime === null; + useEffect(() => { + let interval: NodeJS.Timeout | undefined; + if (isLive) { + setDuration( + differenceInMilliseconds(new Date(), new Date(bout.startTime)), + ); + interval = setInterval(() => { + setDuration( + differenceInMilliseconds(new Date(), new Date(bout.startTime)), + ); + }, 1000); + } + + return () => { + clearInterval(interval); + }; + }, [bout.startTime, isLive]); + + return ( + + {index + 1} + {bout.id} + {bout.name || feed.name} + + + + + {format(new Date(bout.startTime), "h:mm:ss a O")} + + {new Date(bout.startTime).toLocaleDateString()} + + + + {bout.endTime && ( + <> + {format(new Date(bout.endTime), "h:mm:ss a O")} + + {new Date(bout.endTime).toLocaleDateString()} + + + )} + + + {duration && ( + + + {durationString(duration)} + + + )} + + + + + + + + ); +} diff --git a/ui/src/pages/reports/[candidateId].tsx b/ui/src/pages/reports/[candidateId].tsx index 2b5b2dc83..abf691c37 100644 --- a/ui/src/pages/reports/[candidateId].tsx +++ b/ui/src/pages/reports/[candidateId].tsx @@ -78,6 +78,7 @@ const CandidatePage: NextPageWithLayout = () => { )}
+ {candidate && ( { const date = new Date(timestamp); @@ -34,3 +36,16 @@ export const roundToNearest = ( return new Date(roundedTimestamp); }; + +export function durationString(durationMs: number | null | undefined) { + if (typeof durationMs !== "number") return ""; + const duration = intervalToDuration({ start: 0, end: durationMs }); + const hours = Math.floor(durationMs / 1000 / 60 / 60); + + const zeroPad = (num: number) => String(num).padStart(2, "0"); + const formatted = [hours, duration.minutes ?? 0, duration.seconds ?? 0] + .map(zeroPad) + .join(":"); + + return formatted; +}