Skip to content

feat: add NexusDateRangePicker component for date range selection in …#122

Open
wabinyai wants to merge 6 commits into
stagingfrom
feature/reports-historical-data
Open

feat: add NexusDateRangePicker component for date range selection in …#122
wabinyai wants to merge 6 commits into
stagingfrom
feature/reports-historical-data

Conversation

@wabinyai

@wabinyai wabinyai commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator
  • Implemented a date range picker with presets for Today, Yesterday, Last 7 days, Last 30 days, Last 90 days, and This month.
  • Integrated time selection options for start and end dates.
  • Added validation for date range selection to ensure it does not exceed 90 days.
  • Enhanced UI with a calendar view for better date selection experience.

feat: create ReportDataModal component for generating site reports

  • Developed a modal for selecting sites and configuring report parameters.
  • Included search functionality for filtering sites by country and city.
  • Implemented report generation logic with error handling and loading states.
  • Integrated with the API to fetch site report data based on selected parameters.

refactor: update types for site data and report options

  • Extended SiteData interface to include monthly averages and percentage differences.
  • Added new types for data download requests and responses to streamline API interactions.
  • Updated ReportDataOptions to include frequency and data type selections.

fix: enhance API service for fetching site report data

  • Consolidated map readings fetching logic to improve performance and caching.
  • Implemented error handling for API responses to ensure robust data fetching.
  • Updated the buildSiteReportData function to calculate averages and differences for reporting.

Summary by CodeRabbit

  • New Features
    • Added historical air-quality reports with customizable date ranges (including up to 90 days) and dynamic report period labeling.
    • Introduced a new date-range picker and an enhanced report-data modal with aggregation controls and improved site selection.
    • Extended charts for weekly/monthly period comparisons with period-aware exports and updated AQI/PM2.5 formatting.
    • Added automatic data preloading to improve map/report load responsiveness.
  • Bug Fixes
    • Improved secure proxying for analytics report downloads by refining upstream access handling and allowing the additional upstream endpoint.

@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
air-track Ready Ready Preview, Comment Jul 23, 2026 1:20pm
air-vista Ready Ready Preview, Comment Jul 23, 2026 1:20pm
breeze-mind Ready Ready Preview, Comment Jul 23, 2026 1:20pm
clean-aria Ready Ready Preview, Comment Jul 23, 2026 1:20pm
clean-stats Ready Ready Preview, Comment Jul 23, 2026 1:20pm
frontend Ready Ready Preview, Comment Jul 23, 2026 1:20pm
pure-sphere Ready Ready Preview, Comment Jul 23, 2026 1:20pm

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@wabinyai, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 55706fec-2ca3-42a8-a581-ac26d2456d4b

📥 Commits

Reviewing files that changed from the base of the PR and between 782fe52 and 66d8e8e.

📒 Files selected for processing (3)
  • frontend/src/app/reports/page.tsx
  • frontend/src/components/charts/AirQualityChart.tsx
  • frontend/src/components/reports/NexusDateRangePicker.tsx
📝 Walkthrough

Walkthrough

The frontend adds historical report generation with date and site selection, analytics data retrieval and normalization, shared readings prefetching, and weekly/monthly comparison support across report summaries and charts.

Changes

Historical reporting flow

Layer / File(s) Summary
Report data contracts and pipeline
frontend/src/lib/types.ts, frontend/src/services/apiService.tsx, frontend/src/app/api/airqo/[...path]/route.ts
Adds report request/response types, enables the analytics download proxy, injects the server token, and normalizes downloaded records into SiteData[].
Shared readings cache and warmup
frontend/src/services/apiService.tsx, frontend/src/components/AppDataWarmup.tsx, frontend/src/app/layout.tsx
Shares cached and in-flight map-reading requests and starts prefetching when the root layout mounts.
Date selection and report construction
frontend/src/components/reports/NexusDateRangePicker.tsx, frontend/src/components/reports/ReportDataModal.tsx, frontend/src/app/reports/page.tsx
Adds date-range presets, calendar validation, site selection, report generation, historical state, and controls for switching between historical and latest data.
Temporal and period-aware charts
frontend/src/components/charts/AirQualityChart.tsx, frontend/src/app/reports/page.tsx
Adds temporal aggregation, period-filtered AQI categories, and weekly/monthly comparison calculations, labels, exports, sorting, datasets, and axes.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReportsPage
  participant ReportDataModal
  participant loadHistoricalReportData
  participant AirQoProxy
  participant AnalyticsDataDownload
  participant buildSiteReportData
  ReportsPage->>ReportDataModal: submit date range and site selection
  ReportDataModal->>loadHistoricalReportData: request analytics data
  loadHistoricalReportData->>AirQoProxy: POST analytics/data-download
  AirQoProxy->>AnalyticsDataDownload: forward request with server token
  AnalyticsDataDownload-->>AirQoProxy: return report records
  AirQoProxy-->>loadHistoricalReportData: return response payload
  loadHistoricalReportData->>buildSiteReportData: build normalized site data
  buildSiteReportData-->>ReportsPage: update historical report state
Loading

Possibly related PRs

Poem

Dates unfold, reports arise,
Sites and readings synchronize.
Weekly, monthly charts compare,
Secure tokens travel through the air.
Warm caches help the dashboard glow.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and clearly matches a real part of the change, though it only covers the date range picker and not the full set of updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/reports-historical-data

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • JIRA integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.

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

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/app/reports/page.tsx (1)

1723-1728: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

"Weekly Change" summary card isn't period-aware.

The value already switches on comparisonPeriod via calculateAveragePercentageChange, but this card's title stays hardcoded, so monthly reports show a monthly percentage under a "Weekly Change" label (the Results section at Line 1554 was updated, this one was missed).

✏️ Proposed fix
           <SummaryCard
-            title="Weekly Change"
+            title={comparisonPeriod === "monthly" ? "Monthly Change" : "Weekly Change"}
             value={`${calculateAveragePercentageChange(filteredData).toFixed(2)}%`}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/reports/page.tsx` around lines 1723 - 1728, The SummaryCard
for the “Weekly Change” display uses a hardcoded title despite
calculateAveragePercentageChange respecting comparisonPeriod. Update its title
in the SummaryCard block to derive the period label from comparisonPeriod,
matching the period-aware Results section while leaving value, icon, and trend
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/app/reports/page.tsx`:
- Around line 98-101: Update the comparisonPeriod calculation in the report page
to require the selected date span to satisfy the intended duration or
month-over-month precondition before returning "monthly"; do not rely solely on
startDate and endDate crossing calendar months. Preserve "weekly" for short
cross-month ranges such as Jan 30–Feb 2, while retaining monthly mode for valid
full month-over-month selections.
- Around line 451-459: Update the filter effect around filterSites and
setFilteredData so it no longer depends on selectedSite, while retaining the
stale-selection cleanup logic. Remove selectedSite from the dependency array,
ensuring single-site flows that set filteredData are not overwritten when the
selection changes.

In `@frontend/src/components/charts/AirQualityChart.tsx`:
- Around line 567-569: Update the comparison Y-axis domain calculation in
AirQualityChart so empty chartData produces a valid finite domain instead of
spreading an empty allValues array into Math.max. Preserve the existing 10%
headroom for non-empty data and use an appropriate zero-based fallback when no
comparison values exist.

---

Outside diff comments:
In `@frontend/src/app/reports/page.tsx`:
- Around line 1723-1728: The SummaryCard for the “Weekly Change” display uses a
hardcoded title despite calculateAveragePercentageChange respecting
comparisonPeriod. Update its title in the SummaryCard block to derive the period
label from comparisonPeriod, matching the period-aware Results section while
leaving value, icon, and trend unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3599197a-d118-46e5-ba1c-698808cd0383

📥 Commits

Reviewing files that changed from the base of the PR and between 23ccaf3 and b3d6585.

📒 Files selected for processing (9)
  • frontend/src/app/api/airqo/[...path]/route.ts
  • frontend/src/app/layout.tsx
  • frontend/src/app/reports/page.tsx
  • frontend/src/components/AppDataWarmup.tsx
  • frontend/src/components/charts/AirQualityChart.tsx
  • frontend/src/components/reports/NexusDateRangePicker.tsx
  • frontend/src/components/reports/ReportDataModal.tsx
  • frontend/src/lib/types.ts
  • frontend/src/services/apiService.tsx

Comment thread frontend/src/app/reports/page.tsx Outdated
Comment on lines 451 to +459
useEffect(() => {
const result = filterSites(siteData, filters)
const result = filterSites(customReportData ?? siteData, filters)

setFilteredData(result)
// Reset selected site if it's no longer in filtered data
if (selectedSite && !result.some((site) => getSiteSelectionId(site) === getSiteSelectionId(selectedSite))) {
setSelectedSite(null)
}
}, [filters, siteData, selectedSite])
}, [customReportData, filters, siteData, selectedSite])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching reports/page.tsx:"
fd -a 'page\.tsx$' . | sed 's#^\./##' | grep 'frontend/src/app/reports/page.tsx' || true

echo
echo "Line count:"
wc -l frontend/src/app/reports/page.tsx

echo
echo "Relevant effect/hooks context:"
sed -n '1,80p' frontend/src/app/reports/page.tsx
echo
sed -n '420,470p' frontend/src/app/reports/page.tsx
echo
echo "FilteredData/selectedSite usages:"
rg -n "setFilteredData|setSelectedSite|selectedSite|filteredData|SiteCard" frontend/src/app/reports/page.tsx

Repository: airqo-platform/code-samples

Length of output: 12535


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate SiteCard maybe same file:"
rg -n "onSelect|function SiteCard|const SiteCard|setFilteredData|setSelectedSite" frontend/src/app/reports/page.tsx

echo
echo "Read likely SiteCard section:"
sed -n '1820,1910p' frontend/src/app/reports/page.tsx

echo
echo "Read filterSites helper if present:"
sed -n '1,140p' frontend/src/app/reports/page.tsx

Repository: airqo-platform/code-samples

Length of output: 10443


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "ReportContent opening and state declarations:"
sed -n '80,520p' frontend/src/app/reports/page.tsx

echo
echo "Single-device report generation callers around filters reset/useReportOnPage:"
sed -n '1140,1265p' frontend/src/app/reports/page.tsx

echo
echo "SiteCard signature/body relevant section:"
sed -n '2068,2275p' frontend/src/app/reports/page.tsx

echo
echo "JS semantic probe for state-update + deps after render, then deps array includes changed state:"
node - <<'JS'
// Model the specific flow:
// - render uses filteredData = [site], selectedSite = null
// - onSelect sets filteredData = [site], selectedSite = site
// - changeDetector includes selectedSite, selectedSite is in previous deps, so effect runs
// - effect body sets filteredData = filterSites(customReportData ?? siteData, filters)
let siteData = [{name: "network site"}]
let customReportData = null
let filters = {}
let filteredData = [
  {name: "drilled site"},
]
let selectedSite = {name: "drilled site"}

function filterSites(sites) {
  return sites;
}
function prevSelectedSite() { return undefined } // selectedSite was not previously selected

const changedDeps = []
const currentDeps = [customReportData, filters, siteData, selectedSite]
const depsDifffer = filteredData.length;
let overwriteTriggered = false
if (false) {
  // omitted
}
selectedSite && selectedSite !== prevSelectedSite() && selectedSite !== undefined && (overwriteTriggered = true)
if (overwriteTriggered) {
  filteredData = filterSites(customReportData ?? siteData, filters)
}
console.log(JSON.stringify({filteredData, selectedSite, overwriteTriggered}))
JS

Repository: airqo-platform/code-samples

Length of output: 31132


Remove the single-site reset from the filter effect.

setSelectedSite is only called inside single-site flows that also set filteredData to that one site. When that selection state changes, this effect re-runs and replaces it with filterSites(customReportData ?? siteData, filters), so the drilldown view renders the full filtered network instead of the selected site. Keep the stale-selection cleanup there, but exclude selectedSite from the dependency array so the filter effect doesn’t overwrite those flows.

🐛 Proposed fix
   useEffect(() => {
     const result = filterSites(customReportData ?? siteData, filters)
     setFilteredData(result)
     // Reset selected site if it's no longer in filtered data
     if (selectedSite && !result.some((site) => getSiteSelectionId(site) === getSiteSelectionId(selectedSite))) {
       setSelectedSite(null)
     }
-  }, [customReportData, filters, siteData, selectedSite])
+  }, [customReportData, filters, siteData])
📝 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
useEffect(() => {
const result = filterSites(siteData, filters)
const result = filterSites(customReportData ?? siteData, filters)
setFilteredData(result)
// Reset selected site if it's no longer in filtered data
if (selectedSite && !result.some((site) => getSiteSelectionId(site) === getSiteSelectionId(selectedSite))) {
setSelectedSite(null)
}
}, [filters, siteData, selectedSite])
}, [customReportData, filters, siteData, selectedSite])
useEffect(() => {
const result = filterSites(customReportData ?? siteData, filters)
setFilteredData(result)
// Reset selected site if it's no longer in filtered data
if (selectedSite && !result.some((site) => getSiteSelectionId(site) === getSiteSelectionId(selectedSite))) {
setSelectedSite(null)
}
}, [customReportData, filters, siteData])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/reports/page.tsx` around lines 451 - 459, Update the filter
effect around filterSites and setFilteredData so it no longer depends on
selectedSite, while retaining the stale-selection cleanup logic. Remove
selectedSite from the dependency array, ensuring single-site flows that set
filteredData are not overwritten when the selection changes.

Comment thread frontend/src/components/charts/AirQualityChart.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
frontend/src/components/charts/AirQualityChart.tsx (1)

754-767: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard the Y-axis domain against empty chartData (still open).

With no sites carrying the selected period's averages, chartData is empty, so Math.max(...[]) returns -Infinity and comparisonYAxisDomain becomes [0, -Infinity] — an invalid Recharts domain. This was flagged in a prior review and remains unaddressed; the monthly-mode addition in this PR widens the surface for hitting it (e.g., sites without monthlyAverages populated).

🛡️ Proposed fix
   const allValues = chartData.flatMap((item) => [item.current ?? 0, item.previous ?? 0])
-  const maxComparisonValue = Math.max(...allValues)
+  const maxComparisonValue = allValues.length > 0 ? Math.max(...allValues) : 0
   const comparisonYAxisDomain = [0, Math.ceil(maxComparisonValue * 1.1)]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/charts/AirQualityChart.tsx` around lines 754 - 767,
Update the comparison Y-axis domain calculation near chartData and
maxComparisonValue to handle an empty chartData safely, using a finite
non-negative fallback before applying Math.ceil. Preserve the existing scaling
behavior when values are present and ensure comparisonYAxisDomain is always a
valid Recharts domain.
🧹 Nitpick comments (1)
frontend/src/components/charts/AirQualityChart.tsx (1)

37-66: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

getAqiPeriodBucket's "weekly" grouping doesn't match getReportBucket's weekly definition.

getReportBucket buckets by calendar week (shifted to Monday), but getAqiPeriodBucket buckets by naive "week N of month" (Math.ceil(date.getUTCDate() / 7)), which doesn't align to weekday boundaries. On the same report page, the PM2.5 chart's "week" and the AQI category chart's "week" now mean different things, which is confusing when comparing the two.

♻️ Suggested direction
 const getAqiPeriodBucket = (timestamp: string, grouping: "monthly" | "weekly") => {
-  const date = new Date(timestamp)
-  const year = date.getUTCFullYear()
-  const month = date.getUTCMonth()
-  const monthLabel = date.toLocaleDateString("en-US", { month: "long", year: "numeric", timeZone: "UTC" })
-  if (grouping === "monthly") {
-    ...
-  }
-
-  const weekOfMonth = Math.ceil(date.getUTCDate() / 7)
-  return {
-    key: `${year}-${String(month + 1).padStart(2, "0")}-week-${weekOfMonth}`,
-    label: `Week ${weekOfMonth}, ${monthLabel}`,
-    sortValue: Date.UTC(year, month, (weekOfMonth - 1) * 7 + 1),
-  }
+  // reuse the same Monday-aligned weekly bucketing as getReportBucket for consistency
 }

Also applies to: 68-87

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/charts/AirQualityChart.tsx` around lines 37 - 66,
Align the weekly branch of getAqiPeriodBucket with getReportBucket by deriving
each bucket from the calendar week’s Monday in UTC, rather than using
Math.ceil(date.getUTCDate() / 7). Use matching Monday-based keys, labels, and
sort values so AQI and PM2.5 weekly groupings represent the same periods; leave
daily and monthly behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/components/reports/NexusDateRangePicker.tsx`:
- Around line 186-192: Update the range validation used by rangeError and
handleApply to detect when includeTime is enabled and the selected dates are the
same day but startTime is later than endTime. Return an appropriate range error
and prevent onApply from receiving an inverted range, while preserving the
existing missing-end-date and maximum-range checks.

In `@frontend/src/components/reports/ReportDataModal.tsx`:
- Around line 181-188: Wrap the Select callback in the aggregation control so
its plain string value is adapted to ReportDataOptions["frequency"] before
calling onAggregationChange. Update the Select element’s onValueChange prop and
preserve the existing daily, weekly, and monthly options.

---

Duplicate comments:
In `@frontend/src/components/charts/AirQualityChart.tsx`:
- Around line 754-767: Update the comparison Y-axis domain calculation near
chartData and maxComparisonValue to handle an empty chartData safely, using a
finite non-negative fallback before applying Math.ceil. Preserve the existing
scaling behavior when values are present and ensure comparisonYAxisDomain is
always a valid Recharts domain.

---

Nitpick comments:
In `@frontend/src/components/charts/AirQualityChart.tsx`:
- Around line 37-66: Align the weekly branch of getAqiPeriodBucket with
getReportBucket by deriving each bucket from the calendar week’s Monday in UTC,
rather than using Math.ceil(date.getUTCDate() / 7). Use matching Monday-based
keys, labels, and sort values so AQI and PM2.5 weekly groupings represent the
same periods; leave daily and monthly behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b61670b-6f20-40f0-9b07-056111686b89

📥 Commits

Reviewing files that changed from the base of the PR and between b3d6585 and 21a8f2b.

📒 Files selected for processing (6)
  • frontend/src/app/reports/page.tsx
  • frontend/src/components/charts/AirQualityChart.tsx
  • frontend/src/components/reports/NexusDateRangePicker.tsx
  • frontend/src/components/reports/ReportDataModal.tsx
  • frontend/src/lib/types.ts
  • frontend/src/services/apiService.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/src/lib/types.ts
  • frontend/src/services/apiService.tsx
  • frontend/src/app/reports/page.tsx

Comment thread frontend/src/components/reports/NexusDateRangePicker.tsx Outdated
Comment on lines +181 to +188
<Select value={aggregation} onValueChange={onAggregationChange}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="daily">Day</SelectItem>
<SelectItem value="weekly">Week</SelectItem>
<SelectItem value="monthly">Month</SelectItem>
</SelectContent>
</Select>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does assigning a function with a narrower union parameter type to a callback prop typed (value: string) => void cause a TypeScript strictFunctionTypes error?

💡 Result:

Yes, enabling strictFunctionTypes causes a TypeScript error when assigning a function with a narrower parameter type to a callback prop typed with a broader parameter type [1][2]. Under default TypeScript settings, function parameters are checked bivariantly, which allows assigning a function with a narrower parameter (e.g., string) to a type expecting a broader one (e.g., string | number) [3][2]. This behavior is considered unsound because the callback may be invoked with a value (e.g., a number) that the narrower function implementation is not designed to handle [1][4]. When strictFunctionTypes is enabled (or when using strict mode), TypeScript enforces contravariant parameter checking for non-method function types [1][5]. Under these stricter rules, the assignment is rejected because the source function type is not a subtype of the target function type [1][6]. Note that this stricter checking applies to functions defined using function syntax (including callback parameters), but does not apply to functions defined using method syntax in interfaces or classes, which remain bivariant even when strictFunctionTypes is enabled [1][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg '(^|/)(ReportDataModal|select)\.tsx$|(^|/)tsconfig|package\.json$|pnpm-lock|package-lock|yarn.lock' | sed -n '1,120p'

echo
echo "== ReportDataModal relevant lines =="
if [ -f frontend/src/components/reports/ReportDataModal.tsx ]; then
  nl -ba frontend/src/components/reports/ReportDataModal.tsx | sed -n '1,240p'
fi

echo
echo "== select wrapper lines =="
if [ -f frontend/src/ui/select.tsx ]; then
  nl -ba frontend/src/ui/select.tsx | sed -n '1,230p'
fi

echo
echo "== tsconfig references =="
for f in $(git ls-files | rg '(^|/)tsconfig.*\.json$|(^|/)package\.json$|(^|/)pnpm-lock\.yaml$|(^|/)package-lock\.json$|(^|/)yarn\.lock$'); do
  case "$f" in
    *frontend*) echo "--- $f" ;;
    *package.json*) echo "--- $f" ;;
    *tsconfig*.json*) echo "--- $f" ;;
  esac
  sed -n '1,220p' "$f" | rg -n '"extends"|"rootDir"|"noEmit"|"strictFunctionTypes"|"strict"|"strictNullChecks"|"composite"|"typescript"|"radix-ui|Select' || true
done

echo
echo "== all onValueChange SelectItem/value change patterns in frontend/src =="
rg -n "SelectItem value=|onValueChange=|use.*Change|select.tsx" frontend/src || true

Repository: airqo-platform/code-samples

Length of output: 940


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ReportDataModal relevant lines =="
awk '{printf "%6d: %s\n", NR, $0}' frontend/src/components/reports/ReportDataModal.tsx | sed -n '90,210p'

echo
echo "== select wrapper lines =="
awk '{printf "%6d: %s\n", NR, $0}' frontend/src/ui/select.tsx | sed -n '1,220p'

echo
echo "== frontend/tsconfig.json =="
awk '{printf "%6d: %s\n", NR, $0}' frontend/tsconfig.json

echo
echo "== package TypeScript/Radix versions =="
node - <<'JS'
const fs = require('fs');
[
  'frontend/package.json',
  'frontend/package-lock.json'
].forEach(file => {
  const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
  console.log('---', file);
  try {
    if (file.endsWith('package.json')) {
      console.log(JSON.stringify(pkg.dependencies || {}, null, 2));
      console.log(JSON.stringify(pkg.devDependencies || {}, null, 2));
    } else {
      const pkgs = {};
      for (const [k,v] of Object.entries(pkg.packages || {})) {
        if (k === 'node_modules/typescript' || k.includes('node_modules/@radix-ui/react-select') || k.includes('node_modules/@radix-ui/react-popover') || k.includes('node_modules/@radix-ui/react-primitive')) {
          pkgs[k] = v.version || pkg.version;
        }
      }
      console.log(JSON.stringify(pkgs, null, 2));
    }
  } catch (e) {
    console.log('read_parse_error', e.toString());
  }
});
JS

echo
echo "== select callback occurrences =="
rg -n "onValueChange|SelectItem value=|onDataTypeChange|onAggregationChange" frontend/src/components frontend/src/ui || true

Repository: airqo-platform/code-samples

Length of output: 23457


🌐 Web query:

TypeScript strictFunctionTypes function literal parameter variance error assigning callback with narrower parameter type

💡 Result:

When the strictFunctionTypes compiler option is enabled in TypeScript, the language enforces contravariant checking for function parameters, which correctly prevents unsafe assignments [1][2][3]. Understanding the Error In default TypeScript mode, function parameters are checked bivariantly, meaning a function (x: SubType) => void is considered assignable to (x: SuperType) => void, and vice-versa [2][4]. While this is technically unsound, it is permitted to support common JavaScript patterns [4][5]. With strictFunctionTypes enabled, TypeScript switches to strict contravariant checking for function parameters [2][3]. Under these rules, a function type (a: A) => void is assignable to (a: B) => void if and only if B is a subtype of A (i.e., A is a supertype of B) [6][7]. If you attempt to assign a callback with a narrower parameter type (e.g., expecting a Dog) to a variable or location expecting a wider parameter type (e.g., accepting any Animal), TypeScript will correctly flag this as an error because it is unsafe [1][6]. The runtime could pass an Animal that is not a Dog to the callback, causing a crash or unexpected behavior when the code attempts to access Dog-specific properties [1][8]. Key Nuances and Exceptions 1. Method Syntax Exception: To prevent breaking common patterns in generic interfaces and classes (like Array), the strictFunctionTypes flag only applies to functions defined via function type syntax (e.g., (x: T) => void) [1][2][3]. Functions defined using method syntax (e.g., { func(x: T): void }) remain bivariant even when strictFunctionTypes is enabled [1][2]. 2. Callback Parameters: When using strictFunctionTypes, callback parameters are also checked strictly (contravariantly) to ensure type safety in higher-order functions [9]. To resolve these errors, you generally need to ensure your function parameter types are as general as the interface requires, or use generics to accurately define the relationship between the expected and provided types [6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ReportDataModal top declarations and remaining relevant lines =="
awk '{printf "%6d: %s\n", NR, $0}' frontend/src/components/reports/ReportDataModal.tsx | sed -n '1,90p'
awk '{printf "%6d: %s\n", NR, $0}' frontend/src/components/reports/ReportDataModal.tsx | sed -n '210,245p'

echo
echo "== package manager / lockfile TS/Radix exact entries for frontend =="
node - <<'JS'
const fs = require('fs');
const lock = JSON.parse(fs.readFileSync('frontend/package-lock.json', 'utf8'));
const packages = lock.packages || {};
const keys = ['/typescript', '/@radix-ui/react-select', '/@radix-ui/react-popover', '/@radix-ui/react-primitive'];
for (const key of keys) {
  const pkg = packages[key];
  console.log(key, JSON.stringify(pkg && {version: pkg.version, dependencies: pkg.dependencies, peerDependencies: pkg.peerDependencies}.version || null));
}
JS

echo
echo "== lockfile `@types` entries relevant to Radix/typescript **if present** =="
rg -n '"`@types/react`"|"node_modules/@types/react"|node_modules/react-select|typescript' frontend/package-lock.json | head -n 80 || true

Repository: airqo-platform/code-samples

Length of output: 9707


Wrap the aggregation callback for Select.onValueChange.

Select inherits Radix’s plain string onValueChange, while onAggregationChange only accepts "daily" | "weekly" | "monthly". Use an adapter like onValueChange={(value) => onAggregationChange(value as ReportDataOptions["frequency"])} to avoid a strictFunctionTypes assignment error under the existing frontend/tsconfig.json.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/reports/ReportDataModal.tsx` around lines 181 - 188,
Wrap the Select callback in the aggregation control so its plain string value is
adapted to ReportDataOptions["frequency"] before calling onAggregationChange.
Update the Select element’s onValueChange prop and preserve the existing daily,
weekly, and monthly options.

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.

1 participant