Conversation
WalkthroughThis PR adds optional date range filtering to DAG run listings. It introduces a Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Model as DagRunModel
participant Popup as DateFilterPopup
participant Worker as WorkerThread
participant Client as AirflowClient
participant API as Airflow API
User->>Model: Press 'd'
Model->>Popup: new(current_date_filter)
Popup-->>Model: DateFilterPopup created
rect rgba(200, 150, 100, 0.5)
Note over User,Popup: User navigates & selects dates
User->>Popup: Arrow/h/j/k/l keys
Popup->>Popup: Update cursor position
User->>Popup: Space to select date
Popup->>Popup: Set start/end date
User->>Popup: Tab to toggle field
Popup->>Popup: Switch between Start/End
User->>Popup: Enter to confirm
end
Popup-->>Model: Confirmed with date range
Model->>Model: Update date_filter field
Model->>Worker: Queue UpdateDagRuns {dag_id, date_filter}
Worker->>Client: list_dagruns(dag_id, date_filter)
Client->>Client: Build query with date filters
Client->>API: GET /dags/{dag_id}/dagRuns?logical_date_gte=...&logical_date_lte=...
API-->>Client: DagRunList
Client-->>Worker: Return results
Worker->>Model: Update displayed dag runs
Model-->>User: Refresh UI with filtered runs
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
Add a date range filter to the DAG runs panel using ratatui's calendar widget. Users can press 'd' to open a calendar popup where they can select start and end dates to filter which DAG runs are fetched from the Airflow API. Key changes: - Add DagRunDateFilter struct to pass date range to API calls - Update DagRunOperations trait and V1/V2 client implementations to accept and apply date filter query parameters - Create DateFilterPopup with interactive calendar navigation (h/l: ±day, j/k: ±week, H/L: ±month, Space: select, Tab: toggle field, x: clear, Enter: apply) - Wire date filter through WorkerMessage and worker handlers - Show active date filter range in the panel title bar - Add 'd' keybinding and help entry for the date filter command https://claude.ai/code/session_017JgxKcniaEptjNFaQ8iPbU
ecd0690 to
d254bd1
Compare
Fixes clippy::default_trait_access warnings when running with -D warnings. https://claude.ai/code/session_017JgxKcniaEptjNFaQ8iPbU
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (4)
src/airflow/traits/dagrun.rs (1)
7-12: Consider derivingPartialEq+Eqto enable filter-change detectionWithout these derives, callers cannot cheaply check whether the applied filter actually changed (e.g., to avoid issuing a redundant API call when the user re-applies the same dates). Given the struct contains only
Option<time::Date>, both are trivially derivable.♻️ Proposed refactor
-#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct DagRunDateFilter { pub start_date: Option<Date>, pub end_date: Option<Date>, }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/airflow/traits/dagrun.rs` around lines 7 - 12, Add Eq and PartialEq derives to the DagRunDateFilter struct so callers can cheaply compare filters; update the struct declaration (DagRunDateFilter with fields start_date and end_date) to derive PartialEq and Eq in addition to Debug, Clone, Default.src/app/worker/dagruns.rs (1)
93-94: Post-trigger refresh ignores the active date filter.
handle_trigger_dag_runrefreshes the DAG runs list usingDagRunDateFilter::default(), which discards any active filter the user may have set. This could cause a brief flash of unfiltered results until the next periodic tick re-applies the correct filter.Consider threading the user's current
date_filterthrough theTriggerDagRunworker message so the post-trigger refresh respects the active filter.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/worker/dagruns.rs` around lines 93 - 94, handle_trigger_dag_run currently calls handle_update_dag_runs(..., &DagRunDateFilter::default()), which drops the user's active filter; modify the TriggerDagRun worker message to carry the current date_filter (e.g., a DagRunDateFilter field), update places that construct/send TriggerDagRun to populate that field, and change handle_trigger_dag_run to forward the provided date_filter to handle_update_dag_runs instead of using DagRunDateFilter::default(); ensure the TriggerDagRun struct and its handler signature are updated consistently so the post-trigger refresh respects the active date filter.src/app/worker/mod.rs (1)
110-110: Dedup key ignoresdate_filter— a filter change may be briefly delayed.The
..wildcard means that if a periodicUpdateDagRunsfor the samedag_idis already in-flight when the user applies a new date filter, the immediate refresh request will be silently dropped. The next tick will correct this, so the impact is a brief delay (≤ one refresh interval). This is likely fine, but worth being aware of.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/worker/mod.rs` at line 110, The dedup key for UpdateDagRuns currently ignores the date_filter because the pattern uses `..`, causing requests with different filters to collide; change the match arm for `UpdateDagRuns` to destructure and include the `date_filter` (serialized or Option string) in the dedup key (e.g., `format!("UpdateDagRuns:{dag_id}:{date_filter}")`) so each distinct filter produces a unique key; specifically update the match arm that returns Some(format!(...)) to capture the `date_filter` field from the `UpdateDagRuns` variant and incorporate it into the formatted key.src/app/model/popup/dagruns/date_filter.rs (1)
307-325: Range highlighting iterates every day between start and end.For very large date ranges (e.g., spanning years), this loop adds thousands of entries to
CalendarEventStore, though only ~31 are visible. In practice, DAG run filters are unlikely to span years, so this is fine — but if it ever becomes a concern, you could clamp the iteration to the displayed month.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/model/popup/dagruns/date_filter.rs` around lines 307 - 325, The loop currently iterates every day between self.start_date and self.end_date and adds each to events via events.add, which can create thousands of entries for multi-year ranges; instead clamp the iteration to the visible/displayed month (or a fixed ~31-day window) around the calendar view before looping. Concretely, compute a visible_start and visible_end based on the current calendar view derived from self.cursor_date (e.g., first and last day of the displayed month or a 31-day window centered on self.cursor_date), then set the iteration bounds to max(range_start, visible_start) and min(range_end, visible_end) and iterate from that clamped start using d.next_day(), calling events.add only for dates inside the clamped range (still skipping self.cursor_date/self.start_date/self.end_date as before).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/airflow/client/v1/dagrun.rs`:
- Around line 28-32: When building the execution_date_lte query in dagrun.rs,
ensure we don't silently drop the upper bound when date_filter.end_date is
Date::MAX: call end.next_day() as before, but if it returns None fall back to
using end itself (i.e., push query.push(("execution_date_lte",
format!("{end}"))) instead of doing nothing); update the block around
date_filter.end_date and the call to next_day() so the "execution_date_lte"
filter is always added using either next_day or the original end value.
- Around line 25-32: The query currently sends date-only strings from
date_filter.start_date and date_filter.end_date (and end.next_day()) for
execution_date_gte/execution_date_lte, but Airflow expects full RFC3339
datetimes; convert the dates to ISO-8601 datetimes with timezone before pushing
to query (e.g., treat start as start_date at 00:00:00+00:00 and use next_day at
00:00:00+00:00 for the end-inclusive bound), using your project's date/time
serializer (e.g., to_rfc3339 / chrono::DateTime formatting) so
execution_date_gte and execution_date_lte are full datetime strings.
In `@src/app/model/popup/dagruns/date_filter.rs`:
- Around line 106-117: The date selection allows start_date and end_date to be
set in any order which can produce an inverted range; update to_date_filter() to
normalize the range by checking self.start_date and self.end_date and swapping
them when start_date > end_date so the returned filter always has start ≤ end;
keep select_current_date() and DateField logic as-is but ensure to_date_filter()
uses the normalized values (refer to to_date_filter, start_date, end_date, and
DateField) before constructing the filter.
---
Duplicate comments:
In `@src/airflow/client/v2/dagrun.rs`:
- Around line 25-31: The date-only values in dagrun.rs are being formatted as
bare YYYY-MM-DD for query params (in the block checking date_filter.start_date
and date_filter.end_date), causing incorrect filtering; update the formatting
for start and next_day to produce full RFC-like datetimes by appending
"T00:00:00+00:00" (e.g., format!("{start}T00:00:00+00:00")), and for the end
branch use the same next_day() -> unwrap_or(end) fallback used in V1 so that
Date::MAX is handled (i.e., compute next_day().unwrap_or(end) and format that to
logical_date_lte); adjust the query pushes for logical_date_gte and
logical_date_lte accordingly.
---
Nitpick comments:
In `@src/airflow/traits/dagrun.rs`:
- Around line 7-12: Add Eq and PartialEq derives to the DagRunDateFilter struct
so callers can cheaply compare filters; update the struct declaration
(DagRunDateFilter with fields start_date and end_date) to derive PartialEq and
Eq in addition to Debug, Clone, Default.
In `@src/app/model/popup/dagruns/date_filter.rs`:
- Around line 307-325: The loop currently iterates every day between
self.start_date and self.end_date and adds each to events via events.add, which
can create thousands of entries for multi-year ranges; instead clamp the
iteration to the visible/displayed month (or a fixed ~31-day window) around the
calendar view before looping. Concretely, compute a visible_start and
visible_end based on the current calendar view derived from self.cursor_date
(e.g., first and last day of the displayed month or a 31-day window centered on
self.cursor_date), then set the iteration bounds to max(range_start,
visible_start) and min(range_end, visible_end) and iterate from that clamped
start using d.next_day(), calling events.add only for dates inside the clamped
range (still skipping self.cursor_date/self.start_date/self.end_date as before).
In `@src/app/worker/dagruns.rs`:
- Around line 93-94: handle_trigger_dag_run currently calls
handle_update_dag_runs(..., &DagRunDateFilter::default()), which drops the
user's active filter; modify the TriggerDagRun worker message to carry the
current date_filter (e.g., a DagRunDateFilter field), update places that
construct/send TriggerDagRun to populate that field, and change
handle_trigger_dag_run to forward the provided date_filter to
handle_update_dag_runs instead of using DagRunDateFilter::default(); ensure the
TriggerDagRun struct and its handler signature are updated consistently so the
post-trigger refresh respects the active date filter.
In `@src/app/worker/mod.rs`:
- Line 110: The dedup key for UpdateDagRuns currently ignores the date_filter
because the pattern uses `..`, causing requests with different filters to
collide; change the match arm for `UpdateDagRuns` to destructure and include the
`date_filter` (serialized or Option string) in the dedup key (e.g.,
`format!("UpdateDagRuns:{dag_id}:{date_filter}")`) so each distinct filter
produces a unique key; specifically update the match arm that returns
Some(format!(...)) to capture the `date_filter` field from the `UpdateDagRuns`
variant and incorporate it into the formatted key.
| if let Some(start) = date_filter.start_date { | ||
| query.push(("execution_date_gte", format!("{start}"))); | ||
| } | ||
| if let Some(end) = date_filter.end_date { | ||
| // Add one day to make the end date inclusive | ||
| if let Some(next_day) = end.next_day() { | ||
| query.push(("execution_date_lte", format!("{next_day}"))); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Airflow REST API v1 execution_date_gte execution_date_lte parameter format date datetime
💡 Result:
For Airflow Stable REST API v1 list endpoints (e.g. GET /api/v1/dags/{dag_id}/dagRuns), the execution_date_gte / execution_date_lte filters are query parameters whose schema is type: string, format: date-time (OpenAPI “date-time”). [1]
That means you should send an RFC 3339 / ISO-8601 datetime with timezone, e.g. Z (UTC) or an offset like +00:00: [2]
2023-02-02T00:00:00Z2023-02-02T00:00:00+00:002023-02-02T00:00:00.123456+00:00(fractional seconds are fine)
Example:
curl -u airflow:airflow \
"http://localhost:8080/api/v1/dags/demand_forecast/dagRuns?execution_date_gte=2023-02-02T00:00:00Z&execution_date_lte=2023-02-03T00:00:00Z"Also: don’t put these in the JSON body of a GET; they need to be in the URL query string. [3]
Sources: [1] [2] [3]
🌐 Web query:
Airflow 2.x /api/v1 dagRuns list endpoint date filter format requirements
💡 Result:
For Airflow 2.x GET /api/v1/dags/{dag_id}/dagRuns, the “date” filters are query parameters (not JSON body) and must be passed as an ISO-8601 / RFC3339 date-time string (i.e., OpenAPI format: date-time, including a timezone like Z or an offset). (fossies.org)
Which filter params (2.x)
Depending on your Airflow 2.x minor version, you’ll typically use these query params (all date-time):
logical_date_gte,logical_date_lte(newer 2.x;execution_datewas replaced bylogical_date) (fossies.org)start_date_gte/lte,end_date_gte/lte,run_after_gte/lte(fossies.org)
(Older 2.x may still document/useexecution_date_gte/lte; newer versions moved tological_date_*.) (astronomer.io)
Accepted timestamp examples
Use RFC3339 like:
2024-10-01T13:00:00Z2024-10-01T13:00:00+00:00
BothZand+00:00forms are accepted in inputs in newer releases. (github.com)
Example request
curl -X GET \
"http://localhost:8080/api/v1/dags/my_dag/dagRuns?logical_date_gte=2024-10-01T00:00:00Z&logical_date_lte=2024-10-31T23:59:59Z"Note: if you include an offset with + (e.g. +00:00) in a URL, it should be URL-encoded (otherwise + may be treated as a space).
Citations:
- 1: https://fossies.org/linux/apache-airflow/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-rest-api-generated.yaml?utm_source=openai
- 2: https://fossies.org/linux/apache-airflow/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-rest-api-generated.yaml?utm_source=openai
- 3: https://fossies.org/linux/apache-airflow/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-rest-api-generated.yaml?utm_source=openai
- 4: https://www.astronomer.io/docs/software/0.36/airflow-api/?utm_source=openai
- 5: https://github.com/apache/airflow-client-python/releases?utm_source=openai
🏁 Script executed:
# Search the codebase to understand the DagRunDateFilter type and how dates are handled
rg -A 5 "struct DagRunDateFilter" src/Repository: jvanbuel/flowrs
Length of output: 364
🏁 Script executed:
# Check the time crate version and Date Display implementation
cat -n Cargo.toml | grep -A 2 "time"Repository: jvanbuel/flowrs
Length of output: 158
Date format mismatch — Airflow /api/v1 requires full datetime, not date-only string
The current code sends date-only strings (e.g., 2023-02-02) for execution_date_gte/lte parameters, but the Airflow v1 REST API spec defines these as format: date-time, requiring RFC3339 / ISO-8601 datetime with timezone (e.g., 2023-02-02T00:00:00Z or 2023-02-02T00:00:00+00:00). A bare date will be rejected by the API.
Fix by appending the time component:
Proposed fix
- if let Some(start) = date_filter.start_date {
- query.push(("execution_date_gte", format!("{start}")));
- }
- if let Some(end) = date_filter.end_date {
- // Add one day to make the end date inclusive
- if let Some(next_day) = end.next_day() {
- query.push(("execution_date_lte", format!("{next_day}")));
- }
- }
+ if let Some(start) = date_filter.start_date {
+ query.push(("execution_date_gte", format!("{start}T00:00:00Z")));
+ }
+ if let Some(end) = date_filter.end_date {
+ // Add one day to make the end date inclusive
+ if let Some(next_day) = end.next_day() {
+ query.push(("execution_date_lte", format!("{next_day}T00:00:00Z")));
+ }
+ }📝 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.
| if let Some(start) = date_filter.start_date { | |
| query.push(("execution_date_gte", format!("{start}"))); | |
| } | |
| if let Some(end) = date_filter.end_date { | |
| // Add one day to make the end date inclusive | |
| if let Some(next_day) = end.next_day() { | |
| query.push(("execution_date_lte", format!("{next_day}"))); | |
| } | |
| if let Some(start) = date_filter.start_date { | |
| query.push(("execution_date_gte", format!("{start}T00:00:00Z"))); | |
| } | |
| if let Some(end) = date_filter.end_date { | |
| // Add one day to make the end date inclusive | |
| if let Some(next_day) = end.next_day() { | |
| query.push(("execution_date_lte", format!("{next_day}T00:00:00Z"))); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/airflow/client/v1/dagrun.rs` around lines 25 - 32, The query currently
sends date-only strings from date_filter.start_date and date_filter.end_date
(and end.next_day()) for execution_date_gte/execution_date_lte, but Airflow
expects full RFC3339 datetimes; convert the dates to ISO-8601 datetimes with
timezone before pushing to query (e.g., treat start as start_date at
00:00:00+00:00 and use next_day at 00:00:00+00:00 for the end-inclusive bound),
using your project's date/time serializer (e.g., to_rfc3339 / chrono::DateTime
formatting) so execution_date_gte and execution_date_lte are full datetime
strings.
| if let Some(end) = date_filter.end_date { | ||
| // Add one day to make the end date inclusive | ||
| if let Some(next_day) = end.next_day() { | ||
| query.push(("execution_date_lte", format!("{next_day}"))); | ||
| } |
There was a problem hiding this comment.
Silent end-date filter drop when end_date is time::Date::MAX
When end_date == Date::MAX (9999-12-31), next_day() returns None and the upper-bound filter is silently dropped, making the query unbounded upward even though the user explicitly selected an end date. While practically unreachable through a calendar picker, the silent fallback is worth making explicit.
💡 Proposed fix — fall back to the selected end date itself
if let Some(end) = date_filter.end_date {
// Add one day to make the end date inclusive
- if let Some(next_day) = end.next_day() {
- query.push(("execution_date_lte", format!("{next_day}T00:00:00+00:00")));
- }
+ let bound = end.next_day().unwrap_or(end);
+ query.push(("execution_date_lte", format!("{bound}T00:00:00+00:00")));
}📝 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.
| if let Some(end) = date_filter.end_date { | |
| // Add one day to make the end date inclusive | |
| if let Some(next_day) = end.next_day() { | |
| query.push(("execution_date_lte", format!("{next_day}"))); | |
| } | |
| if let Some(end) = date_filter.end_date { | |
| // Add one day to make the end date inclusive | |
| let bound = end.next_day().unwrap_or(end); | |
| query.push(("execution_date_lte", format!("{bound}"))); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/airflow/client/v1/dagrun.rs` around lines 28 - 32, When building the
execution_date_lte query in dagrun.rs, ensure we don't silently drop the upper
bound when date_filter.end_date is Date::MAX: call end.next_day() as before, but
if it returns None fall back to using end itself (i.e., push
query.push(("execution_date_lte", format!("{end}"))) instead of doing nothing);
update the block around date_filter.end_date and the call to next_day() so the
"execution_date_lte" filter is always added using either next_day or the
original end value.
| fn select_current_date(&mut self) { | ||
| match self.active_field { | ||
| DateField::Start => { | ||
| self.start_date = Some(self.cursor_date); | ||
| // Auto-advance to end date selection | ||
| self.active_field = DateField::End; | ||
| } | ||
| DateField::End => { | ||
| self.end_date = Some(self.cursor_date); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
No validation that start_date ≤ end_date.
A user can Tab back to the Start field and select a date after the End date (or vice versa). to_date_filter() will produce a filter where start > end, which will likely yield zero results from the API. The range rendering (Line 309) handles this by swapping, but the filter itself is not normalized.
Consider swapping the dates in to_date_filter() if they're inverted:
🛡️ Proposed fix
pub fn to_date_filter(&self) -> DagRunDateFilter {
+ // Normalize so start <= end
+ let (start, end) = match (self.start_date, self.end_date) {
+ (Some(s), Some(e)) if s > e => (Some(e), Some(s)),
+ other => other,
+ };
DagRunDateFilter {
- start_date: self.start_date,
- end_date: self.end_date,
+ start_date: start,
+ end_date: end,
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/model/popup/dagruns/date_filter.rs` around lines 106 - 117, The date
selection allows start_date and end_date to be set in any order which can
produce an inverted range; update to_date_filter() to normalize the range by
checking self.start_date and self.end_date and swapping them when start_date >
end_date so the returned filter always has start ≤ end; keep
select_current_date() and DateField logic as-is but ensure to_date_filter() uses
the normalized values (refer to to_date_filter, start_date, end_date, and
DateField) before constructing the filter.
Summary
This PR adds a date range filter feature for DAG runs, allowing users to filter the DAG run list by start and end dates using an interactive calendar popup.
Key Changes
New Date Filter Popup (
src/app/model/popup/dagruns/date_filter.rs):DAG Run Model Integration (
src/app/model/dagruns.rs):date_filterfield to store the current date range filterAPI Layer Updates:
DagRunOperationstrait to acceptDagRunDateFilterparameterexecution_date_gte/execution_date_lteparameterslogical_date_gte/logical_date_lteparametersWorker Message Updates (
src/app/worker/mod.rs):UpdateDagRunsmessage to includedate_filterfieldUI/UX Enhancements:
Implementation Details
DagRunDateFilterstruct is a simple optional date range container that can be serialized/clonedMonthlywidget with custom event stylinghttps://claude.ai/code/session_017JgxKcniaEptjNFaQ8iPbU
Summary by CodeRabbit
Release Notes
New Features
Chores