bbovenzi commented on code in PR #70470:
URL: https://github.com/apache/airflow/pull/70470#discussion_r3722892232


##########
airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx:
##########
@@ -314,6 +316,7 @@ export const DagsList = () => {
     dagDisplayNamePattern: Boolean(dagDisplayNamePattern) ? 
dagDisplayNamePattern : undefined,
     dagRunsLimit,
     dagRunState,
+    dagRunStateWithinHours: Boolean(dagRunState) && withinHours !== null ? 
Number(withinHours) : undefined,

Review Comment:
   Let's make sure that `Number(withinHours)` is returning a valid number.



##########
airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/RunStateScopeSelect.tsx:
##########
@@ -0,0 +1,91 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { HStack, Text, type Select as ChakraSelect } from "@chakra-ui/react";
+import { createListCollection } from "@chakra-ui/react/collection";
+import { useTranslation } from "react-i18next";
+import { FiClock, FiZap } from "react-icons/fi";
+
+import { Select } from "src/components/ui";
+
+// "latest" matches on the latest run only; numeric values are hours; "any" 
has no time bound.
+export type RunStateScope = "168" | "24" | "720" | "any" | "latest";
+
+type Props = {
+  readonly dataTestId?: string;
+  readonly onChange: (value: RunStateScope) => void;
+  readonly triggerProps?: ChakraSelect.TriggerProps;
+  readonly value: RunStateScope;
+};
+
+const SCOPE_OPTIONS: ReadonlyArray<{ labelKey: string; value: RunStateScope }> 
= [
+  { labelKey: "latestRun", value: "latest" },
+  { labelKey: "last24Hours", value: "24" },
+  { labelKey: "last7Days", value: "168" },
+  { labelKey: "last30Days", value: "720" },
+  { labelKey: "anyTime", value: "any" },
+];
+
+export const RunStateScopeSelect = ({ dataTestId, onChange, triggerProps, 
value }: Props) => {

Review Comment:
   Nit. I think this variable name is confusing. Let's do 
`RunStateLookbackSelect`



##########
airflow-core/src/airflow/api_fastapi/common/parameters.py:
##########
@@ -1473,19 +1473,29 @@ def depends(cls, has_pending_actions: bool | None = 
Query(None)) -> _PendingActi
 QueryPendingActionsFilter = Annotated[_PendingActionsFilter, 
Depends(_PendingActionsFilter.depends)]
 
 
+# A lookback this large is effectively unbounded (users omit the param for 
"any time"); capping it
+# also keeps utcnow() - timedelta(hours=...) from overflowing on an absurdly 
large value.
+_MAX_DAG_RUN_STATE_WINDOW_HOURS = 24 * 366 * 100  # ~100 years
+
+
 class _AnyDagRunStateFilter(BaseParam[DagRunState | None]):
     """Filter Dags that have any DagRun in the given state, not only the 
latest one."""
 
+    def __init__(self, value: DagRunState | None = None, skip_none: bool = 
True) -> None:
+        super().__init__(value, skip_none)
+        self.within_hours: int | None = None
+
     def to_orm(self, select: Select) -> Select:
         if self.value is None and self.skip_none:
             return select
 
-        # EXISTS resolves each Dag via the (dag_id, state) index instead of 
scanning every run in the state.
-        has_run_in_state = (
-            sql_select(DagRun.dag_id)
-            .where(DagRun.dag_id == DagModel.dag_id, DagRun.state == 
self.value)
-            .exists()
-        )
+        # EXISTS seeks the (dag_id, state) index per Dag rather than scanning 
the whole table; the
+        # optional run_after bound is not covered by that index, so it is 
filtered within each Dag's
+        # matching rows (still bounded per Dag, not a full scan).
+        conditions = [DagRun.dag_id == DagModel.dag_id, DagRun.state == 
self.value]
+        if self.within_hours is not None:
+            conditions.append(DagRun.run_after >= timezone.utcnow() - 
timedelta(hours=self.within_hours))
+        has_run_in_state = 
sql_select(DagRun.dag_id).where(*conditions).exists()

Review Comment:
   Without  new index and for time ranges, I think we should invert this 
subquery to look at the dag runs in the range first instead of checking the 
history of every each dag.
   
   ```
           bound = timezone.utcnow() - timedelta(hours=self.within_hours)
           dag_ids_in_window = (
               sql_select(DagRun.dag_id)
               .where(DagRun.run_after >= bound, DagRun.state == self.value)
               .distinct()
           )
   ```
   
   



##########
airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/runStateFilter.ts:
##########
@@ -0,0 +1,51 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { RunStateScope } from "./RunStateScopeSelect";
+
+// The unified "Run state" control is backed by the existing independent URL 
params:
+//   - "latest" scope  → last_dag_run_state  (match the latest run only)
+//   - a time scope     → dag_run_state       (match any run) + a within-hours 
bound
+//     ("any" is any-run with no time bound, so it carries no window param)
+// An unset key means "clear this param".
+export type RunStateSelection = {
+  readonly dagRunState?: string;
+  readonly dagRunStateWithinHours?: string;
+  readonly lastDagRunState?: string;
+};
+
+export const runStateSelectionFor = (state: string | undefined, scope: 
RunStateScope): RunStateSelection => {
+  if (state === undefined) {
+    return {};
+  }
+  if (scope === "latest") {
+    return { lastDagRunState: state };
+  }
+  if (scope === "any") {
+    return { dagRunState: state };
+  }
+
+  return { dagRunState: state, dagRunStateWithinHours: scope };
+};
+
+export const runScopeFor = (
+  lastRunState: string | null,
+  anyRunState: string | null,
+  anyRunStateWindow: string | null,
+): RunStateScope =>
+  lastRunState !== null || anyRunState === null ? "latest" : 
((anyRunStateWindow ?? "any") as RunStateScope);

Review Comment:
   Let's make sure we validate the run state window is a correct number here 
too.



##########
airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx:
##########
@@ -67,14 +70,20 @@ export const DagsFilters = () => {
   const showPaused = searchParams.get(PAUSED_PARAM);
   const showFavorites = searchParams.get(FAVORITE_PARAM);
   const needsReview = searchParams.get(NEEDS_REVIEW_PARAM);
-  const state = searchParams.get(LAST_DAG_RUN_STATE_PARAM);
-  const activeRunState = searchParams.get(DAG_RUN_STATE_PARAM);
+  const lastRunState = searchParams.get(LAST_DAG_RUN_STATE_PARAM);
+  const anyRunState = searchParams.get(DAG_RUN_STATE_PARAM);
+  const anyRunStateWindow = searchParams.get(DAG_RUN_STATE_WITHIN_HOURS_PARAM);

Review Comment:
   Let's call everything a "window", a "period", or a "lookback" but let's not 
mix our variable names or "scope".



##########
airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json:
##########
@@ -24,6 +24,15 @@
       "paused": "Paused"
     },
     "runIdPatternFilter": "Search Dag Runs",
+    "runScope": {
+      "anyTime": "Any time",
+      "in": "in",
+      "last7Days": "Last 7 days",
+      "last24Hours": "Last 24 hours",
+      "last30Days": "Last 30 days",
+      "latestRun": "Latest run"
+    },
+    "runState": "Run state",

Review Comment:
   We have a lot of these translation keys already. The dashboard page has a 
time range selector.



##########
airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/RunStateScopeSelect.tsx:
##########
@@ -0,0 +1,91 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { HStack, Text, type Select as ChakraSelect } from "@chakra-ui/react";
+import { createListCollection } from "@chakra-ui/react/collection";
+import { useTranslation } from "react-i18next";
+import { FiClock, FiZap } from "react-icons/fi";
+
+import { Select } from "src/components/ui";
+
+// "latest" matches on the latest run only; numeric values are hours; "any" 
has no time bound.
+export type RunStateScope = "168" | "24" | "720" | "any" | "latest";
+
+type Props = {
+  readonly dataTestId?: string;
+  readonly onChange: (value: RunStateScope) => void;
+  readonly triggerProps?: ChakraSelect.TriggerProps;
+  readonly value: RunStateScope;
+};
+
+const SCOPE_OPTIONS: ReadonlyArray<{ labelKey: string; value: RunStateScope }> 
= [
+  { labelKey: "latestRun", value: "latest" },
+  { labelKey: "last24Hours", value: "24" },
+  { labelKey: "last7Days", value: "168" },
+  { labelKey: "last30Days", value: "720" },

Review Comment:
   We should try to reuse the time ranges that the dashboard and overview page 
already use. I could see a world where we move it into the airflow config so 
deployment managers can decide based on how often their runs fire. Some teams 
do every minute, others every day.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to