This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 7c648ab63a4 Add filter with run_after field in dags/:dagid/runs page
(#62797)
7c648ab63a4 is described below
commit 7c648ab63a45acbbe64bdb8d5ff69c79617eb636
Author: Prajwal7842 <[email protected]>
AuthorDate: Wed Mar 18 22:45:02 2026 +0530
Add filter with run_after field in dags/:dagid/runs page (#62797)
* Add filter for run_after field in dags/:dagid/runs page
* Use existing date time picker for run after filter
* Fix overflow for filter when vertical space is limited
---
.../src/airflow/ui/src/constants/localStorage.ts | 2 +
.../ui/src/layouts/Details/DetailsLayout.tsx | 10 +++
.../airflow/ui/src/layouts/Details/Grid/Grid.tsx | 13 +++-
.../ui/src/layouts/Details/PanelButtons.tsx | 71 +++++++++++++++++++++-
.../src/airflow/ui/src/queries/useGridRuns.ts | 6 ++
5 files changed, 99 insertions(+), 3 deletions(-)
diff --git a/airflow-core/src/airflow/ui/src/constants/localStorage.ts
b/airflow-core/src/airflow/ui/src/constants/localStorage.ts
index 72fd47b0909..ab8398438d9 100644
--- a/airflow-core/src/airflow/ui/src/constants/localStorage.ts
+++ b/airflow-core/src/airflow/ui/src/constants/localStorage.ts
@@ -34,6 +34,8 @@ export const dagRunsLimitKey = (dagId: string) =>
`dag_runs_limit-${dagId}`;
export const runTypeFilterKey = (dagId: string) => `run_type_filter-${dagId}`;
export const triggeringUserFilterKey = (dagId: string) =>
`triggering_user_filter-${dagId}`;
export const dagRunStateFilterKey = (dagId: string) =>
`dag_run_state_filter-${dagId}`;
+export const runAfterGteKey = (dagId: string) => `run_after_gte-${dagId}`;
+export const runAfterLteKey = (dagId: string) => `run_after_lte-${dagId}`;
export const showGanttKey = (dagId: string) => `show_gantt-${dagId}`;
export const dependenciesKey = (dagId: string) => `dependencies-${dagId}`;
export const directionKey = (dagId: string) => `direction-${dagId}`;
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
index 71635ea6dbf..075f710b10f 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
@@ -48,6 +48,8 @@ import {
dagRunStateFilterKey,
dagViewKey,
DEFAULT_DAG_VIEW_KEY,
+ runAfterGteKey,
+ runAfterLteKey,
runTypeFilterKey,
showGanttKey,
triggeringUserFilterKey,
@@ -77,6 +79,8 @@ export const DetailsLayout = ({ children, error, isLoading,
tabs }: Props) => {
const panelGroupRef = useRef<ImperativePanelGroupHandle | null>(null);
const [dagView, setDagView] = useLocalStorage<"graph" |
"grid">(dagViewKey(dagId), defaultDagView);
const [limit, setLimit] = useLocalStorage<number>(dagRunsLimitKey(dagId),
10);
+ const [runAfterGte, setRunAfterGte] = useLocalStorage<string |
undefined>(runAfterGteKey(dagId), undefined);
+ const [runAfterLte, setRunAfterLte] = useLocalStorage<string |
undefined>(runAfterLteKey(dagId), undefined);
const [runTypeFilter, setRunTypeFilter] = useLocalStorage<DagRunType |
undefined>(
runTypeFilterKey(dagId),
undefined,
@@ -163,10 +167,14 @@ export const DetailsLayout = ({ children, error,
isLoading, tabs }: Props) => {
dagView={dagView}
limit={limit}
panelGroupRef={panelGroupRef}
+ runAfterGte={runAfterGte}
+ runAfterLte={runAfterLte}
runTypeFilter={runTypeFilter}
setDagRunStateFilter={setDagRunStateFilter}
setDagView={setDagView}
setLimit={setLimit}
+ setRunAfterGte={setRunAfterGte}
+ setRunAfterLte={setRunAfterLte}
setRunTypeFilter={setRunTypeFilter}
setShowGantt={setShowGantt}
setShowVersionIndicatorMode={setShowVersionIndicatorMode}
@@ -182,6 +190,8 @@ export const DetailsLayout = ({ children, error, isLoading,
tabs }: Props) => {
<Grid
dagRunState={dagRunStateFilter}
limit={limit}
+ runAfterGte={runAfterGte}
+ runAfterLte={runAfterLte}
runType={runTypeFilter}
showGantt={Boolean(runId) && showGantt}
showVersionIndicatorMode={showVersionIndicatorMode}
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx
index 07537c2cb2b..dbe30598010 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx
@@ -53,6 +53,8 @@ dayjs.extend(dayjsDuration);
type Props = {
readonly dagRunState?: DagRunState | undefined;
readonly limit: number;
+ readonly runAfterGte?: string;
+ readonly runAfterLte?: string;
readonly runType?: DagRunType | undefined;
readonly showGantt?: boolean;
readonly showVersionIndicatorMode?: VersionIndicatorOptions;
@@ -62,6 +64,8 @@ type Props = {
export const Grid = ({
dagRunState,
limit,
+ runAfterGte,
+ runAfterLte,
runType,
showGantt,
showVersionIndicatorMode,
@@ -82,7 +86,14 @@ export const Grid = ({
const depthParam = searchParams.get("depth");
const depth = depthParam !== null && depthParam !== "" ?
parseInt(depthParam, 10) : undefined;
- const { data: gridRuns, isLoading } = useGridRuns({ dagRunState, limit,
runType, triggeringUser });
+ const { data: gridRuns, isLoading } = useGridRuns({
+ dagRunState,
+ limit,
+ runAfterGte,
+ runAfterLte,
+ runType,
+ triggeringUser,
+ });
// Check if the selected dag run is inside of the grid response, if not,
we'll update the grid filters
// Eventually we should redo the api endpoint to make this work better
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
index 3b2334b5170..860f9a4d200 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
@@ -43,6 +43,9 @@ import { useLocalStorage } from "usehooks-ts";
import type { DagRunState, DagRunType } from "openapi/requests/types.gen";
import { DagVersionSelect } from "src/components/DagVersionSelect";
+import { DateRangeCalendar } from
"src/components/FilterBar/filters/DateRangeCalendar";
+import { DateRangeInputs } from
"src/components/FilterBar/filters/DateRangeInputs";
+import type { DateRangeValue } from "src/components/FilterBar/types";
import { directionOptions, type Direction } from
"src/components/Graph/useGraphLayout";
import { RunTypeIcon } from "src/components/RunTypeIcon";
import { SearchBar } from "src/components/SearchBar";
@@ -53,6 +56,7 @@ import { Checkbox } from "src/components/ui/Checkbox";
import { dependenciesKey, directionKey } from "src/constants/localStorage";
import type { VersionIndicatorOptions } from
"src/constants/showVersionIndicatorOptions";
import { dagRunTypeOptions, dagRunStateOptions } from
"src/constants/stateOptions";
+import { useDateRangeFilter } from "src/hooks/useDateRangeFilter";
import { useContainerWidth } from "src/utils/useContainerWidth";
import { DagRunSelect } from "./DagRunSelect";
@@ -66,10 +70,14 @@ type Props = {
readonly dagView: "graph" | "grid";
readonly limit: number;
readonly panelGroupRef: React.RefObject<ImperativePanelGroupHandle | null>;
+ readonly runAfterGte: string | undefined;
+ readonly runAfterLte: string | undefined;
readonly runTypeFilter: DagRunType | undefined;
readonly setDagRunStateFilter:
React.Dispatch<React.SetStateAction<DagRunState | undefined>>;
readonly setDagView: (x: "graph" | "grid") => void;
readonly setLimit: React.Dispatch<React.SetStateAction<number>>;
+ readonly setRunAfterGte: React.Dispatch<React.SetStateAction<string |
undefined>>;
+ readonly setRunAfterLte: React.Dispatch<React.SetStateAction<string |
undefined>>;
readonly setRunTypeFilter: React.Dispatch<React.SetStateAction<DagRunType |
undefined>>;
readonly setShowGantt: React.Dispatch<React.SetStateAction<boolean>>;
readonly setShowVersionIndicatorMode:
React.Dispatch<React.SetStateAction<VersionIndicatorOptions>>;
@@ -117,10 +125,14 @@ export const PanelButtons = ({
dagView,
limit,
panelGroupRef,
+ runAfterGte,
+ runAfterLte,
runTypeFilter,
setDagRunStateFilter,
setDagView,
setLimit,
+ setRunAfterGte,
+ setRunAfterLte,
setRunTypeFilter,
setShowGantt,
setShowVersionIndicatorMode,
@@ -129,7 +141,7 @@ export const PanelButtons = ({
showVersionIndicatorMode,
triggeringUserFilter,
}: Props) => {
- const { t: translate } = useTranslation(["components", "dag"]);
+ const { t: translate } = useTranslation(["common", "components", "dag"]);
const { dagId = "", runId } = useParams();
const { fitView } = useReactFlow();
const shouldShowToggleButtons = Boolean(runId);
@@ -201,6 +213,30 @@ export const PanelButtons = ({
setTriggeringUserFilter(trimmedValue === "" ? undefined : trimmedValue);
};
+ const runAfterRange: DateRangeValue = {
+ endDate: runAfterLte,
+ startDate: runAfterGte,
+ };
+
+ const handleRunAfterRangeChange = (next: DateRangeValue) => {
+ setRunAfterGte(next.startDate);
+ setRunAfterLte(next.endDate);
+ };
+
+ const {
+ editingState,
+ endDateValue,
+ getFieldError,
+ handleDateClick: handleRunAfterDateClick,
+ handleInputChange: handleRunAfterInputChange,
+ setEditingState,
+ startDateValue,
+ } = useDateRangeFilter({
+ onChange: handleRunAfterRangeChange,
+ translate,
+ value: runAfterRange,
+ });
+
const handleFocus = (view: string) => {
if (panelGroupRef.current) {
const newLayout = view === "graph" ? [70, 30] : [30, 70];
@@ -274,7 +310,14 @@ export const PanelButtons = ({
<Popover.Positioner>
<Popover.Content>
<Popover.Arrow />
- <Popover.Body display="flex" flexDirection="column" gap={4}
p={2}>
+ <Popover.Body
+ display="flex"
+ flexDirection="column"
+ gap={4}
+ maxH="70vh"
+ overflowY="auto"
+ p={2}
+ >
{dagView === "graph" ? (
<>
<DagVersionSelect />
@@ -470,6 +513,30 @@ export const PanelButtons = ({
placeholder={translate("common:dagRun.triggeringUser")}
/>
</VStack>
+ <VStack alignItems="flex-start">
+ <Text fontSize="xs" mb={1}>
+ {translate("common:dagRun.runAfter")}
+ </Text>
+ <DateRangeInputs
+ editingState={editingState}
+ endDateValue={endDateValue}
+ getFieldError={getFieldError}
+ handleInputChange={handleRunAfterInputChange}
+ onChange={handleRunAfterRangeChange}
+ setEditingState={setEditingState}
+ startDateValue={startDateValue}
+ translate={translate}
+ value={runAfterRange}
+ />
+ <DateRangeCalendar
+ currentMonth={editingState.currentMonth}
+ onDateClick={handleRunAfterDateClick}
+ onMonthChange={(month) =>
+ setEditingState((prev) => ({ ...prev,
currentMonth: month }))
+ }
+ value={runAfterRange}
+ />
+ </VStack>
{shouldShowToggleButtons ? (
<VStack alignItems="flex-start" px={1}>
<Checkbox checked={showGantt} onChange={() =>
setShowGantt(!showGantt)} size="sm">
diff --git a/airflow-core/src/airflow/ui/src/queries/useGridRuns.ts
b/airflow-core/src/airflow/ui/src/queries/useGridRuns.ts
index 35508b8ecfa..69f3df332ae 100644
--- a/airflow-core/src/airflow/ui/src/queries/useGridRuns.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useGridRuns.ts
@@ -25,11 +25,15 @@ import { isStatePending, useAutoRefresh } from "src/utils";
export const useGridRuns = ({
dagRunState,
limit,
+ runAfterGte,
+ runAfterLte,
runType,
triggeringUser,
}: {
dagRunState?: DagRunState | undefined;
limit: number;
+ runAfterGte?: string;
+ runAfterLte?: string;
runType?: DagRunType | undefined;
triggeringUser?: string | undefined;
}) => {
@@ -42,6 +46,8 @@ export const useGridRuns = ({
dagId,
limit,
orderBy: ["-run_after"],
+ runAfterGte: runAfterGte ?? undefined,
+ runAfterLte: runAfterLte ?? undefined,
runType: runType ? [runType] : undefined,
state: dagRunState ? [dagRunState] : undefined,
triggeringUser: triggeringUser ?? undefined,