Skip to content

Add new ReportBoutList in the candidate report page#909

Open
skanderm wants to merge 1 commit intomainfrom
skander/bouts-in-report
Open

Add new ReportBoutList in the candidate report page#909
skanderm wants to merge 1 commit intomainfrom
skander/bouts-in-report

Conversation

@skanderm
Copy link
Contributor

@skanderm skanderm commented Aug 18, 2025

image

Summary by CodeRabbit

  • New Features

    • Candidate reports now show a table of related bouts filtered by detection category and time window.
    • Each bout includes a quick link to open its details.
  • Enhancements

    • Durations display in a consistent HH:MM:SS format across bout views.
    • Ongoing bouts update their duration live.
    • Clearer feedback while loading and when no bouts are available.

@skanderm skanderm requested a review from paulcretu as a code owner August 18, 2025 23:58
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 18, 2025

Walkthrough

Consolidates duration formatting into a shared util, widens DetectionsTable props, introduces ReportBoutList to fetch and render bouts for a candidate’s category/time window, adds enum conversion helpers, and makes a whitespace-only change in a page.

Changes

Cohort / File(s) Summary
Duration util extraction
ui/src/components/Bouts/BoutItem.tsx, ui/src/utils/time.ts
Moved inline durationString to shared util in utils/time.ts; BoutItem now imports and uses it. New exported durationString uses date-fns intervalToDuration.
Detections table integration
ui/src/components/DetectionsTable.tsx
Expanded props: feed now includes id,name; candidate now includes minTime,maxTime,category. Conditionally renders ReportBoutList when category is present.
New reports module
ui/src/modules/reports/ReportBoutList.tsx
New component fetching bouts via useBoutsQuery filtered by category/time; renders table with BoutItem rows, loading/empty states, live-updating duration for ongoing bouts.
Enum mappers
ui/src/utils/enum.ts
Added helpers to map between DetectionCategory and AudioCategory in both directions.
Whitespace
ui/src/pages/reports/[candidateId].tsx
Inserted a blank line; no functional changes.

Sequence Diagram(s)

sequenceDiagram
  participant U as User
  participant DT as DetectionsTable
  participant RBL as ReportBoutList
  participant Q as useBoutsQuery
  participant GQL as GraphQL API
  participant BI as BoutItem
  participant T as Timer

  U->>DT: Open candidate report
  DT->>RBL: Render when candidate.category exists (feed,id/name,min/max,category)
  RBL->>Q: useBoutsQuery({ feedId, category, minTime, maxTime })
  Q->>GQL: Fetch bouts with filters
  GQL-->>Q: Bouts data
  Q-->>RBL: {loading|data}
  RBL->>BI: Render BoutItem per bout
  loop Ongoing bouts
    T-->>BI: tick every 1s
    BI->>BI: Recompute duration
  end
  U-->>RBL: View table and actions
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • paulcretu

Poem

A rabbit taps the timelike drum—tick, tock, tick!
Durations hop to shared fields, neat and slick.
Bouts line up, in rows they gleam,
Categories mapped—what a tidy scheme!
With every second, lives a spark—
Reports now shine from light to dark. 🐇⏱️

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch skander/bouts-in-report

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@paulcretu paulcretu temporarily deployed to orcasite-pr-909 August 19, 2025 00:00 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (11)
ui/src/utils/enum.ts (1)

6-10: Consider adding runtime validation for unmapped enum values.

While the current implementation works for known enum values, it could benefit from handling unexpected values more gracefully.

Consider adding runtime validation to handle unmapped enum values:

 export function detectionCategoryToAudioCategory(
   detectionCategory: DetectionCategory,
 ): AudioCategory {
-  return {
+  const mapping = {
     WHALE: AudioCategory.Biophony,
     VESSEL: AudioCategory.Anthrophony,
     OTHER: AudioCategory.Geophony,
-  }[detectionCategory];
+  };
+  
+  const result = mapping[detectionCategory as keyof typeof mapping];
+  if (result === undefined) {
+    throw new Error(`Unknown DetectionCategory: ${detectionCategory}`);
+  }
+  return result;
 }

 export function audioCategoryToDetectionCategory(
   audioCategory: AudioCategory,
 ): DetectionCategory {
-  return {
+  const mapping = {
     BIOPHONY: DetectionCategory.Whale,
     ANTHROPHONY: DetectionCategory.Vessel,
     GEOPHONY: DetectionCategory.Other,
-  }[audioCategory];
+  };
+  
+  const result = mapping[audioCategory as keyof typeof mapping];
+  if (result === undefined) {
+    throw new Error(`Unknown AudioCategory: ${audioCategory}`);
+  }
+  return result;
 }

Also applies to: 16-20

ui/src/modules/reports/ReportBoutList.tsx (10)

50-52: Potentially excludes live bouts (null endTime) from results.

The filter endTime >= minTime will typically exclude ongoing bouts where endTime is null. If you intend to include bouts that started before maxTime and are still ongoing, add an OR branch that accepts endTime == null.

If your GraphQL filter supports boolean logic, consider something like:

  • startTime <= maxTime AND (endTime >= minTime OR endTime IS NULL)

Please confirm the supported filter syntax (e.g., or: [...], OR: [...], endTime: { isNull: true }) and adjust accordingly.


45-45: Remove leftover debug log.

Console logging feed IDs in production is noisy and can leak implementation details.

Apply this diff to remove it:

-  console.log("feed id?", feed.id);

59-59: Prefer MUI Typography over raw h3 for consistency and theming.

Apply this diff:

-      <h3>Bouts</h3>
+      <Typography variant="h6" component="h3">Bouts</Typography>

20-20: Avoid importing the entire lodash bundle for a simple range.

Reduces bundle size and improves tree-shaking. Replace with native Array.from.

Apply this diff to drop lodash:

-import _ from "lodash";

76-81: Use native Array.from instead of lodash.range.

Removes lodash dependency for this case.

Apply this diff:

-              {_.range(8).map((_, idx) => (
+              {Array.from({ length: 8 }, (_, idx) => (
                 <TableCell key={idx}>
                   <Skeleton animation="wave" variant="text" />
                 </TableCell>
               ))}

83-94: Consider rendering an error state.

If useBoutsQuery exposes error or isError, surface a brief message instead of falling back to “No bouts” when the query fails.

Example (non-diff, adjust to your hook API):

const { data: boutData, isLoading, error } = useBoutsQuery(/* ... */);
// ...
{!isLoading && error && (
  <TableRow>
    <TableCell colSpan={8} sx={{ textAlign: "center", color: "error.main" }}>
      Failed to load bouts
    </TableCell>
  </TableRow>
)}

133-133: Redundant key on TableRow inside a keyed list item.

The key on BoutItem at the call site is sufficient. Remove the inner key to avoid confusion.

Apply this diff:

-    <TableRow key={bout.id}>
+    <TableRow>

115-115: Timer typing and cleanup guard for the interval.

Using NodeJS.Timeout in the browser can cause type friction; guard clearInterval against undefined.

Apply this diff:

-    let interval: NodeJS.Timeout | undefined;
+    let interval: ReturnType<typeof setInterval> | undefined;
@@
-    return () => {
-      clearInterval(interval);
-    };
+    return () => {
+      if (interval) {
+        clearInterval(interval);
+      }
+    };

Also applies to: 127-129


1-13: Use Next.js Link for client-side navigation and prefetch.

Switching to next/link avoids full page reloads and improves UX.

Apply these diffs:

 } from "@mui/material";
+import NextLink from "next/link";
-        <Link href={`/bouts/${bout.id}`} title={`/bouts/${bout.id}`}>
+        <Link component={NextLink} href={`/bouts/${bout.id}`} title={`/bouts/${bout.id}`}>
           <Button size="small">View</Button>
         </Link>

Also applies to: 171-174


101-109: Tighten BoutItem props typing to match list query results.

Using BoutQuery["bout"] couples this to a different query shape. Prefer the generated Bout type or derive the item type from useBoutsQuery results for better resilience.

For example (adjust to your generated types):

  • If exported: import type { Bout } from "@/graphql/generated"; then bout: Pick<Bout, "id" | "name" | "category" | "startTime" | "endTime" | "duration">
  • Or derive: type BoutListItem = NonNullable<NonNullable<ReturnType<typeof useBoutsQuery>["data"]>["bouts"]["results"]>[number];
    Would you like me to push a typed version aligned to your codegen?
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b8dcac8 and fde1fcc.

📒 Files selected for processing (6)
  • ui/src/components/Bouts/BoutItem.tsx (1 hunks)
  • ui/src/components/DetectionsTable.tsx (3 hunks)
  • ui/src/modules/reports/ReportBoutList.tsx (1 hunks)
  • ui/src/pages/reports/[candidateId].tsx (1 hunks)
  • ui/src/utils/enum.ts (1 hunks)
  • ui/src/utils/time.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-11-05T23:46:05.615Z
Learnt from: paulcretu
PR: orcasound/orcasite#694
File: ui/src/components/Bouts/FeedItem.tsx:36-36
Timestamp: 2024-11-05T23:46:05.615Z
Learning: DetectionCategory is a union type, not an enum; initializing arrays of DetectionCategory with string literals is valid.

Applied to files:

  • ui/src/utils/enum.ts
🧬 Code Graph Analysis (1)
ui/src/modules/reports/ReportBoutList.tsx (3)
ui/src/graphql/generated/index.ts (2)
  • BoutQuery (3308-3332)
  • Feed (879-902)
ui/src/utils/enum.ts (1)
  • detectionCategoryToAudioCategory (3-11)
ui/src/utils/time.ts (1)
  • durationString (40-51)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (11)
ui/src/utils/time.ts (2)

1-1: LGTM! Clean import addition for duration formatting.

The import of intervalToDuration from date-fns is correctly added to support the new shared utility function.


40-51: Well-implemented shared duration utility with proper null handling.

The durationString function is well-structured with:

  • Robust null/undefined handling
  • Proper hour calculation using Math.floor
  • Consistent zero-padding for HH:MM:SS format
  • Good use of date-fns for minutes/seconds extraction

This consolidation eliminates code duplication and provides a consistent duration formatting approach across components.

ui/src/components/Bouts/BoutItem.tsx (2)

16-16: Good refactoring to use shared duration utility.

The import of the shared durationString function from @/utils/time successfully eliminates the local duplicate implementation, promoting code reuse and maintainability.


139-139: Please verify the imported durationString matches the original formatting

I wasn’t able to locate a prior, in-file definition of durationString in BoutItem.tsx, nor compare it against the shared version in ui/src/utils/time.ts. To be safe, please:

  • Manually review any existing examples where durationString was used locally and compare outputs against the shared util.
  • Add or update unit tests (if not already in place) to cover key duration cases (e.g., null/undefined, sub-hour, multi-hour spans).
  • Confirm there are no subtle formatting differences introduced by intervalToDuration.
ui/src/pages/reports/[candidateId].tsx (1)

81-81: Minor formatting improvement.

The added blank line improves code readability by creating better visual separation between the main content box and the conditional DetectionsTable rendering.

ui/src/components/DetectionsTable.tsx (3)

31-31: LGTM! Clean import for the new ReportBoutList component.

The import statement correctly references the new ReportBoutList component from the modules directory.


57-61: Well-designed props expansion to support bout rendering.

The expansion of the props interface is thoughtfully designed:

  • feed now includes id and name needed by ReportBoutList for bout queries and display
  • candidate now includes minTime, maxTime, and category required for time-window filtering and category mapping

The type constraints using Pick maintain good encapsulation by only exposing the necessary properties.


189-197: Appropriate conditional rendering of bout information.

The conditional rendering of ReportBoutList when candidate.category exists is well-implemented:

  • Properly guards against undefined category values
  • Passes all required props (feed, time window, category)
  • Maintains the existing table functionality while adding new bout visualization

The integration follows React patterns well and provides additional context for candidates with categorized detections.

ui/src/utils/enum.ts (2)

3-11: Well-structured enum conversion with clear mapping.

The detectionCategoryToAudioCategory function provides a clean mapping:

  • WHALE → AudioCategory.Biophony
  • VESSEL → AudioCategory.Anthrophony
  • OTHER → AudioCategory.Geophony

The implementation is concise and uses proper TypeScript enum references.


13-21: Symmetric conversion function with correct reverse mapping.

The audioCategoryToDetectionCategory function correctly provides the inverse mapping:

  • BIOPHONY → DetectionCategory.Whale
  • ANTHROPHONY → DetectionCategory.Vessel
  • GEOPHONY → DetectionCategory.Other

Both functions together provide bidirectional conversion between the enum types, which is useful for data transformation between different parts of the system.

ui/src/modules/reports/ReportBoutList.tsx (1)

49-49: Good use of enum mapping for query filter.

Mapping DetectionCategory to AudioCategory via detectionCategoryToAudioCategory keeps the filter consistent with backend expectations.

Comment on lines +156 to +169
<TableCell>
{duration && (
<Box
sx={{ mt: { sm: "auto" }, ml: { xs: "auto", sm: 0 } }}
title={formatDuration(
intervalToDuration({ start: 0, end: duration }),
)}
>
<Typography variant="monospace" textAlign="right">
{durationString(duration)}
</Typography>
</Box>
)}
</TableCell>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Duration not shown for 0ms and invalid Typography variant.

  • The truthy check hides “00:00:00” when duration is 0.
  • Typography variant "monospace" is not a valid MUI variant; use a standard variant and set fontFamily.

Apply this diff:

-      <TableCell>
-        {duration && (
+      <TableCell>
+        {duration != null && (
           <Box
             sx={{ mt: { sm: "auto" }, ml: { xs: "auto", sm: 0 } }}
             title={formatDuration(
               intervalToDuration({ start: 0, end: duration }),
             )}
           >
-            <Typography variant="monospace" textAlign="right">
+            <Typography variant="body2" sx={{ fontFamily: "monospace" }} textAlign="right">
               {durationString(duration)}
             </Typography>
           </Box>
         )}
       </TableCell>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<TableCell>
{duration && (
<Box
sx={{ mt: { sm: "auto" }, ml: { xs: "auto", sm: 0 } }}
title={formatDuration(
intervalToDuration({ start: 0, end: duration }),
)}
>
<Typography variant="monospace" textAlign="right">
{durationString(duration)}
</Typography>
</Box>
)}
</TableCell>
<TableCell>
{duration != null && (
<Box
sx={{ mt: { sm: "auto" }, ml: { xs: "auto", sm: 0 } }}
title={formatDuration(
intervalToDuration({ start: 0, end: duration }),
)}
>
<Typography
variant="body2"
sx={{ fontFamily: "monospace" }}
textAlign="right"
>
{durationString(duration)}
</Typography>
</Box>
)}
</TableCell>
🤖 Prompt for AI Agents
In ui/src/modules/reports/ReportBoutList.tsx around lines 156 to 169, the
current truthy check hides a 0ms duration and the Typography uses an invalid MUI
variant "monospace"; change the conditional to explicitly check for
null/undefined (e.g., duration !== null && duration !== undefined) so 0 is
rendered, and replace the Typography variant with a valid variant such as
"body2" (or "caption") while applying sx={{ fontFamily: 'monospace' }} (or
theme.typography.monospace) to get monospaced styling; keep the existing title
formatting logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants