This is an automated email from the ASF dual-hosted git repository. kaxilnaik pushed a commit to branch v3-1-test in repository https://gitbox.apache.org/repos/asf/airflow.git
commit d91f7575a5d9aebd8ed9f86e9a1eaf062d8459a9 Author: Wei Lee <[email protected]> AuthorDate: Tue Sep 16 17:59:51 2025 +0800 Add HITLDetail.created_at (#55525) (cherry picked from commit bfc0f8940461eae3b85196e82818c6b9d4bbe376) --- airflow-core/docs/img/airflow_erd.sha256 | 2 +- airflow-core/docs/img/airflow_erd.svg | 121 +++++++++++---------- .../api_fastapi/core_api/datamodels/hitl.py | 1 + .../api_fastapi/core_api/openapi/_private_ui.yaml | 5 + .../core_api/openapi/v2-rest-api-generated.yaml | 41 +++++++ .../api_fastapi/core_api/routes/public/hitl.py | 5 + .../0076_3_1_0_add_human_in_the_loop_response.py | 2 + airflow-core/src/airflow/models/hitl.py | 2 + .../src/airflow/ui/openapi-gen/queries/common.ts | 8 +- .../ui/openapi-gen/queries/ensureQueryData.ts | 12 +- .../src/airflow/ui/openapi-gen/queries/prefetch.ts | 12 +- .../src/airflow/ui/openapi-gen/queries/queries.ts | 12 +- .../src/airflow/ui/openapi-gen/queries/suspense.ts | 12 +- .../airflow/ui/openapi-gen/requests/schemas.gen.ts | 7 +- .../ui/openapi-gen/requests/services.gen.ts | 10 +- .../airflow/ui/openapi-gen/requests/types.gen.ts | 5 + .../core_api/routes/public/test_hitl.py | 34 +++++- .../api_fastapi/core_api/routes/ui/test_dags.py | 1 + .../src/airflowctl/api/datamodels/generated.py | 1 + 19 files changed, 220 insertions(+), 73 deletions(-) diff --git a/airflow-core/docs/img/airflow_erd.sha256 b/airflow-core/docs/img/airflow_erd.sha256 index ba060d4f5ee..bf88ae1d3c4 100644 --- a/airflow-core/docs/img/airflow_erd.sha256 +++ b/airflow-core/docs/img/airflow_erd.sha256 @@ -1 +1 @@ -35e9e07930e138664fb6ff23bc299567a88946734630d84f3d7d95deacf2f4b8 \ No newline at end of file +35b8a7f30e44075373199a53e6634693f4254287a9ecff0582d9ae926fc7aaae \ No newline at end of file diff --git a/airflow-core/docs/img/airflow_erd.svg b/airflow-core/docs/img/airflow_erd.svg index 6fbf9c224a4..c6fc5f0ea7d 100644 --- a/airflow-core/docs/img/airflow_erd.svg +++ b/airflow-core/docs/img/airflow_erd.svg @@ -1437,68 +1437,73 @@ <!-- hitl_detail --> <g id="node43" class="node"> <title>hitl_detail</title> -<polygon fill="none" stroke="black" points="2197,-2304 2197,-2332 2435,-2332 2435,-2304 2197,-2304"/> -<text text-anchor="start" x="2272" y="-2315.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">hitl_detail</text> -<polygon fill="none" stroke="black" points="2197,-2279 2197,-2304 2435,-2304 2435,-2279 2197,-2279"/> -<text text-anchor="start" x="2202" y="-2288.8" font-family="Helvetica,sans-Serif" text-decoration="underline" font-size="14.00">ti_id</text> -<text text-anchor="start" x="2231" y="-2288.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2236" y="-2288.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [UUID]</text> -<text text-anchor="start" x="2288" y="-2288.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> -<polygon fill="none" stroke="black" points="2197,-2254 2197,-2279 2435,-2279 2435,-2254 2197,-2254"/> -<text text-anchor="start" x="2202" y="-2263.8" font-family="Helvetica,sans-Serif" font-size="14.00">assignees</text> -<text text-anchor="start" x="2272" y="-2263.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2277" y="-2263.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> -<polygon fill="none" stroke="black" points="2197,-2229 2197,-2254 2435,-2254 2435,-2229 2197,-2229"/> -<text text-anchor="start" x="2202" y="-2238.8" font-family="Helvetica,sans-Serif" font-size="14.00">body</text> -<text text-anchor="start" x="2237" y="-2238.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2242" y="-2238.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [TEXT]</text> -<polygon fill="none" stroke="black" points="2197,-2204 2197,-2229 2435,-2229 2435,-2204 2197,-2204"/> -<text text-anchor="start" x="2202" y="-2213.8" font-family="Helvetica,sans-Serif" font-size="14.00">chosen_options</text> -<text text-anchor="start" x="2310" y="-2213.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2315" y="-2213.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> -<polygon fill="none" stroke="black" points="2197,-2179 2197,-2204 2435,-2204 2435,-2179 2197,-2179"/> -<text text-anchor="start" x="2202" y="-2188.8" font-family="Helvetica,sans-Serif" font-size="14.00">defaults</text> -<text text-anchor="start" x="2259" y="-2188.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2264" y="-2188.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> -<polygon fill="none" stroke="black" points="2197,-2154 2197,-2179 2435,-2179 2435,-2154 2197,-2154"/> -<text text-anchor="start" x="2202" y="-2163.8" font-family="Helvetica,sans-Serif" font-size="14.00">multiple</text> -<text text-anchor="start" x="2259" y="-2163.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2264" y="-2163.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [BOOLEAN]</text> -<polygon fill="none" stroke="black" points="2197,-2129 2197,-2154 2435,-2154 2435,-2129 2197,-2129"/> -<text text-anchor="start" x="2202" y="-2138.8" font-family="Helvetica,sans-Serif" font-size="14.00">options</text> -<text text-anchor="start" x="2254" y="-2138.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2259" y="-2138.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> -<text text-anchor="start" x="2310" y="-2138.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> -<polygon fill="none" stroke="black" points="2197,-2104 2197,-2129 2435,-2129 2435,-2104 2197,-2104"/> -<text text-anchor="start" x="2202" y="-2113.8" font-family="Helvetica,sans-Serif" font-size="14.00">params</text> -<text text-anchor="start" x="2255" y="-2113.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2260" y="-2113.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> -<text text-anchor="start" x="2311" y="-2113.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> -<polygon fill="none" stroke="black" points="2197,-2079 2197,-2104 2435,-2104 2435,-2079 2197,-2079"/> -<text text-anchor="start" x="2202" y="-2088.8" font-family="Helvetica,sans-Serif" font-size="14.00">params_input</text> -<text text-anchor="start" x="2298" y="-2088.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2303" y="-2088.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> -<text text-anchor="start" x="2354" y="-2088.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> -<polygon fill="none" stroke="black" points="2197,-2054 2197,-2079 2435,-2079 2435,-2054 2197,-2054"/> -<text text-anchor="start" x="2202" y="-2063.8" font-family="Helvetica,sans-Serif" font-size="14.00">responded_at</text> -<text text-anchor="start" x="2296" y="-2063.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2301" y="-2063.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [TIMESTAMP]</text> -<polygon fill="none" stroke="black" points="2197,-2029 2197,-2054 2435,-2054 2435,-2029 2197,-2029"/> -<text text-anchor="start" x="2202" y="-2038.8" font-family="Helvetica,sans-Serif" font-size="14.00">responded_by</text> -<text text-anchor="start" x="2300" y="-2038.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2305" y="-2038.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> -<polygon fill="none" stroke="black" points="2197,-2004 2197,-2029 2435,-2029 2435,-2004 2197,-2004"/> -<text text-anchor="start" x="2202" y="-2013.8" font-family="Helvetica,sans-Serif" font-size="14.00">subject</text> -<text text-anchor="start" x="2253" y="-2013.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> -<text text-anchor="start" x="2258" y="-2013.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [TEXT]</text> -<text text-anchor="start" x="2308" y="-2013.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> +<polygon fill="none" stroke="black" points="2186,-2328 2186,-2356 2446,-2356 2446,-2328 2186,-2328"/> +<text text-anchor="start" x="2272" y="-2339.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">hitl_detail</text> +<polygon fill="none" stroke="black" points="2186,-2303 2186,-2328 2446,-2328 2446,-2303 2186,-2303"/> +<text text-anchor="start" x="2191" y="-2312.8" font-family="Helvetica,sans-Serif" text-decoration="underline" font-size="14.00">ti_id</text> +<text text-anchor="start" x="2220" y="-2312.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2225" y="-2312.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [UUID]</text> +<text text-anchor="start" x="2277" y="-2312.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> +<polygon fill="none" stroke="black" points="2186,-2278 2186,-2303 2446,-2303 2446,-2278 2186,-2278"/> +<text text-anchor="start" x="2191" y="-2287.8" font-family="Helvetica,sans-Serif" font-size="14.00">assignees</text> +<text text-anchor="start" x="2261" y="-2287.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2266" y="-2287.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> +<polygon fill="none" stroke="black" points="2186,-2253 2186,-2278 2446,-2278 2446,-2253 2186,-2253"/> +<text text-anchor="start" x="2191" y="-2262.8" font-family="Helvetica,sans-Serif" font-size="14.00">body</text> +<text text-anchor="start" x="2226" y="-2262.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2231" y="-2262.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [TEXT]</text> +<polygon fill="none" stroke="black" points="2186,-2228 2186,-2253 2446,-2253 2446,-2228 2186,-2228"/> +<text text-anchor="start" x="2191" y="-2237.8" font-family="Helvetica,sans-Serif" font-size="14.00">chosen_options</text> +<text text-anchor="start" x="2299" y="-2237.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2304" y="-2237.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> +<polygon fill="none" stroke="black" points="2186,-2203 2186,-2228 2446,-2228 2446,-2203 2186,-2203"/> +<text text-anchor="start" x="2191" y="-2212.8" font-family="Helvetica,sans-Serif" font-size="14.00">created_at</text> +<text text-anchor="start" x="2264" y="-2212.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2269" y="-2212.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [TIMESTAMP]</text> +<text text-anchor="start" x="2365" y="-2212.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> +<polygon fill="none" stroke="black" points="2186,-2178 2186,-2203 2446,-2203 2446,-2178 2186,-2178"/> +<text text-anchor="start" x="2191" y="-2187.8" font-family="Helvetica,sans-Serif" font-size="14.00">defaults</text> +<text text-anchor="start" x="2248" y="-2187.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2253" y="-2187.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> +<polygon fill="none" stroke="black" points="2186,-2153 2186,-2178 2446,-2178 2446,-2153 2186,-2153"/> +<text text-anchor="start" x="2191" y="-2162.8" font-family="Helvetica,sans-Serif" font-size="14.00">multiple</text> +<text text-anchor="start" x="2248" y="-2162.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2253" y="-2162.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [BOOLEAN]</text> +<polygon fill="none" stroke="black" points="2186,-2128 2186,-2153 2446,-2153 2446,-2128 2186,-2128"/> +<text text-anchor="start" x="2191" y="-2137.8" font-family="Helvetica,sans-Serif" font-size="14.00">options</text> +<text text-anchor="start" x="2243" y="-2137.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2248" y="-2137.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> +<text text-anchor="start" x="2299" y="-2137.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> +<polygon fill="none" stroke="black" points="2186,-2103 2186,-2128 2446,-2128 2446,-2103 2186,-2103"/> +<text text-anchor="start" x="2191" y="-2112.8" font-family="Helvetica,sans-Serif" font-size="14.00">params</text> +<text text-anchor="start" x="2244" y="-2112.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2249" y="-2112.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> +<text text-anchor="start" x="2300" y="-2112.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> +<polygon fill="none" stroke="black" points="2186,-2078 2186,-2103 2446,-2103 2446,-2078 2186,-2078"/> +<text text-anchor="start" x="2191" y="-2087.8" font-family="Helvetica,sans-Serif" font-size="14.00">params_input</text> +<text text-anchor="start" x="2287" y="-2087.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2292" y="-2087.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> +<text text-anchor="start" x="2343" y="-2087.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> +<polygon fill="none" stroke="black" points="2186,-2053 2186,-2078 2446,-2078 2446,-2053 2186,-2053"/> +<text text-anchor="start" x="2191" y="-2062.8" font-family="Helvetica,sans-Serif" font-size="14.00">responded_at</text> +<text text-anchor="start" x="2285" y="-2062.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2290" y="-2062.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [TIMESTAMP]</text> +<polygon fill="none" stroke="black" points="2186,-2028 2186,-2053 2446,-2053 2446,-2028 2186,-2028"/> +<text text-anchor="start" x="2191" y="-2037.8" font-family="Helvetica,sans-Serif" font-size="14.00">responded_by</text> +<text text-anchor="start" x="2289" y="-2037.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2294" y="-2037.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [JSON]</text> +<polygon fill="none" stroke="black" points="2186,-2003 2186,-2028 2446,-2028 2446,-2003 2186,-2003"/> +<text text-anchor="start" x="2191" y="-2012.8" font-family="Helvetica,sans-Serif" font-size="14.00">subject</text> +<text text-anchor="start" x="2242" y="-2012.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> +<text text-anchor="start" x="2247" y="-2012.8" font-family="Helvetica,sans-Serif" font-size="14.00"> [TEXT]</text> +<text text-anchor="start" x="2297" y="-2012.8" font-family="Helvetica,sans-Serif" font-size="14.00"> NOT NULL</text> </g> <!-- task_instance--hitl_detail --> <g id="edge48" class="edge"> <title>task_instance--hitl_detail</title> -<path fill="none" stroke="#7f7f7f" stroke-dasharray="5,2" d="M2081.05,-1862.88C2103.53,-1907.23 2127.94,-1950.81 2154,-1991 2164.45,-2007.12 2176.38,-2023.19 2188.91,-2038.71"/> -<text text-anchor="start" x="2178.91" y="-2027.51" font-family="Times,serif" font-size="14.00">1</text> -<text text-anchor="start" x="2081.05" y="-1851.68" font-family="Times,serif" font-size="14.00">1</text> +<path fill="none" stroke="#7f7f7f" stroke-dasharray="5,2" d="M2081.12,-1859.57C2103.71,-1904.92 2128.13,-1949.64 2154,-1991 2161.37,-2002.79 2169.44,-2014.6 2177.92,-2026.25"/> +<text text-anchor="start" x="2167.92" y="-2015.05" font-family="Times,serif" font-size="14.00">1</text> +<text text-anchor="start" x="2081.12" y="-1848.37" font-family="Times,serif" font-size="14.00">1</text> </g> <!-- task_map --> <g id="node44" class="node"> diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/hitl.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/hitl.py index f24688c13e5..aa4f44f212b 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/hitl.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/hitl.py @@ -63,6 +63,7 @@ class HITLDetail(BaseModel): multiple: bool = False params: dict[str, Any] = Field(default_factory=dict) assigned_users: list[HITLUser] = Field(default_factory=list) + created_at: datetime # Response Content Detail responded_by_user: HITLUser | None = None diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml index 269f60cd4c3..ec079113922 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml @@ -1999,6 +1999,10 @@ components: $ref: '#/components/schemas/HITLUser' type: array title: Assigned Users + created_at: + type: string + format: date-time + title: Created At responded_by_user: anyOf: - $ref: '#/components/schemas/HITLUser' @@ -2029,6 +2033,7 @@ components: - task_instance - options - subject + - created_at title: HITLDetail description: Schema for Human-in-the-loop detail. HITLUser: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml index e853fcbd386..a54062a172a 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml @@ -8236,6 +8236,42 @@ paths: title: Body Search description: "SQL LIKE expression \u2014 use `%` / `_` wildcards (e.g. `%customer_%`).\ \ Regular expressions are **not** supported." + - name: created_at_gte + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Created At Gte + - name: created_at_gt + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Created At Gt + - name: created_at_lte + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Created At Lte + - name: created_at_lt + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Created At Lt responses: '200': description: Successful Response @@ -10857,6 +10893,10 @@ components: $ref: '#/components/schemas/HITLUser' type: array title: Assigned Users + created_at: + type: string + format: date-time + title: Created At responded_by_user: anyOf: - $ref: '#/components/schemas/HITLUser' @@ -10887,6 +10927,7 @@ components: - task_instance - options - subject + - created_at title: HITLDetail description: Schema for Human-in-the-loop detail. HITLDetailCollection: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py index 60d9f99fab9..f4b1e0baae2 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py @@ -38,7 +38,9 @@ from airflow.api_fastapi.common.parameters import ( QueryLimit, QueryOffset, QueryTIStateFilter, + RangeFilter, SortParam, + datetime_range_filter_factory, ) from airflow.api_fastapi.common.router import AirflowRouter from airflow.api_fastapi.core_api.datamodels.hitl import ( @@ -209,6 +211,7 @@ def get_hitl_details( "ti_id", "subject", "responded_at", + "created_at", ], model=HITLDetailModel, to_replace={ @@ -234,6 +237,7 @@ def get_hitl_details( responded_user_name: QueryHITLDetailRespondedUserNameFilter, subject_patten: QueryHITLDetailSubjectSearch, body_patten: QueryHITLDetailBodySearch, + created_at: Annotated[RangeFilter, Depends(datetime_range_filter_factory("created_at", HITLDetailModel))], ) -> HITLDetailCollection: """Get Human-in-the-loop details.""" query = ( @@ -265,6 +269,7 @@ def get_hitl_details( responded_user_name, subject_patten, body_patten, + created_at, ], offset=offset, limit=limit, diff --git a/airflow-core/src/airflow/migrations/versions/0076_3_1_0_add_human_in_the_loop_response.py b/airflow-core/src/airflow/migrations/versions/0076_3_1_0_add_human_in_the_loop_response.py index cd875711ca4..3105d453e45 100644 --- a/airflow-core/src/airflow/migrations/versions/0076_3_1_0_add_human_in_the_loop_response.py +++ b/airflow-core/src/airflow/migrations/versions/0076_3_1_0_add_human_in_the_loop_response.py @@ -32,6 +32,7 @@ from alembic import op from sqlalchemy import Boolean, Column, ForeignKeyConstraint, String, Text from sqlalchemy.dialects import postgresql +from airflow._shared.timezones import timezone from airflow.settings import json from airflow.utils.sqlalchemy import UtcDateTime @@ -60,6 +61,7 @@ def upgrade(): Column("multiple", Boolean, unique=False, default=False), Column("params", sqlalchemy_jsonfield.JSONField(json=json), nullable=False, default={}), Column("assignees", sqlalchemy_jsonfield.JSONField(json=json), nullable=True), + Column("created_at", UtcDateTime(timezone=True), nullable=False, default=timezone.utcnow), Column("responded_at", UtcDateTime, nullable=True), Column("responded_by", sqlalchemy_jsonfield.JSONField(json=json), nullable=True), Column("chosen_options", sqlalchemy_jsonfield.JSONField(json=json), nullable=True), diff --git a/airflow-core/src/airflow/models/hitl.py b/airflow-core/src/airflow/models/hitl.py index f13c4a6117f..b6bbb2bc402 100644 --- a/airflow-core/src/airflow/models/hitl.py +++ b/airflow-core/src/airflow/models/hitl.py @@ -26,6 +26,7 @@ from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import relationship from sqlalchemy.sql.functions import FunctionElement +from airflow._shared.timezones import timezone from airflow.models.base import Base from airflow.settings import json from airflow.utils.sqlalchemy import UtcDateTime @@ -97,6 +98,7 @@ class HITLDetail(Base): multiple = Column(Boolean, unique=False, default=False) params = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=False, default={}) assignees = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=True) + created_at = Column(UtcDateTime, default=timezone.utcnow, nullable=False) # Response Content Detail responded_at = Column(UtcDateTime, nullable=True) diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts index 3a937cdd85f..4cc84c1ad3a 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts @@ -564,8 +564,12 @@ export const UseTaskInstanceServiceGetHitlDetailKeyFn = ({ dagId, dagRunId, mapI export type TaskInstanceServiceGetHitlDetailsDefaultResponse = Awaited<ReturnType<typeof TaskInstanceService.getHitlDetails>>; export type TaskInstanceServiceGetHitlDetailsQueryResult<TData = TaskInstanceServiceGetHitlDetailsDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>; export const useTaskInstanceServiceGetHitlDetailsKey = "TaskInstanceServiceGetHitlDetails"; -export const UseTaskInstanceServiceGetHitlDetailsKeyFn = ({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { +export const UseTaskInstanceServiceGetHitlDetailsKeyFn = ({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { bodySearch?: string; + createdAtGt?: string; + createdAtGte?: string; + createdAtLt?: string; + createdAtLte?: string; dagId: string; dagIdPattern?: string; dagRunId: string; @@ -579,7 +583,7 @@ export const UseTaskInstanceServiceGetHitlDetailsKeyFn = ({ bodySearch, dagId, d subjectSearch?: string; taskId?: string; taskIdPattern?: string; -}, queryKey?: Array<unknown>) => [useTaskInstanceServiceGetHitlDetailsKey, ...(queryKey ?? [{ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }])]; +}, queryKey?: Array<unknown>) => [useTaskInstanceServiceGetHitlDetailsKey, ...(queryKey ?? [{ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }])]; export type ImportErrorServiceGetImportErrorDefaultResponse = Awaited<ReturnType<typeof ImportErrorService.getImportError>>; export type ImportErrorServiceGetImportErrorQueryResult<TData = ImportErrorServiceGetImportErrorDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>; export const useImportErrorServiceGetImportErrorKey = "ImportErrorServiceGetImportError"; diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts index bad321b274c..39129114dc5 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts @@ -1083,11 +1083,19 @@ export const ensureUseTaskInstanceServiceGetHitlDetailData = (queryClient: Query * @param data.respondedByUserName * @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.createdAtGte +* @param data.createdAtGt +* @param data.createdAtLte +* @param data.createdAtLt * @returns HITLDetailCollection Successful Response * @throws ApiError */ -export const ensureUseTaskInstanceServiceGetHitlDetailsData = (queryClient: QueryClient, { bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { +export const ensureUseTaskInstanceServiceGetHitlDetailsData = (queryClient: QueryClient, { bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { bodySearch?: string; + createdAtGt?: string; + createdAtGte?: string; + createdAtLt?: string; + createdAtLte?: string; dagId: string; dagIdPattern?: string; dagRunId: string; @@ -1101,7 +1109,7 @@ export const ensureUseTaskInstanceServiceGetHitlDetailsData = (queryClient: Quer subjectSearch?: string; taskId?: string; taskIdPattern?: string; -}) => queryClient.ensureQueryData({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }), queryFn: () => TaskInstanceService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }) }); +}) => queryClient.ensureQueryData({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }), queryFn: () => TaskInstanceService.getHitlDetails({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, offset, orde [...] /** * Get Import Error * Get an import error. diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts index 0f3565e9fe0..c9d8961be12 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts @@ -1083,11 +1083,19 @@ export const prefetchUseTaskInstanceServiceGetHitlDetail = (queryClient: QueryCl * @param data.respondedByUserName * @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.createdAtGte +* @param data.createdAtGt +* @param data.createdAtLte +* @param data.createdAtLt * @returns HITLDetailCollection Successful Response * @throws ApiError */ -export const prefetchUseTaskInstanceServiceGetHitlDetails = (queryClient: QueryClient, { bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { +export const prefetchUseTaskInstanceServiceGetHitlDetails = (queryClient: QueryClient, { bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { bodySearch?: string; + createdAtGt?: string; + createdAtGte?: string; + createdAtLt?: string; + createdAtLte?: string; dagId: string; dagIdPattern?: string; dagRunId: string; @@ -1101,7 +1109,7 @@ export const prefetchUseTaskInstanceServiceGetHitlDetails = (queryClient: QueryC subjectSearch?: string; taskId?: string; taskIdPattern?: string; -}) => queryClient.prefetchQuery({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }), queryFn: () => TaskInstanceService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }) }); +}) => queryClient.prefetchQuery({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }), queryFn: () => TaskInstanceService.getHitlDetails({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, offset, orderB [...] /** * Get Import Error * Get an import error. diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts index 9d23ddbcfe6..3594ae23797 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts @@ -1083,11 +1083,19 @@ export const useTaskInstanceServiceGetHitlDetail = <TData = Common.TaskInstanceS * @param data.respondedByUserName * @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.createdAtGte +* @param data.createdAtGt +* @param data.createdAtLte +* @param data.createdAtLt * @returns HITLDetailCollection Successful Response * @throws ApiError */ -export const useTaskInstanceServiceGetHitlDetails = <TData = Common.TaskInstanceServiceGetHitlDetailsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { +export const useTaskInstanceServiceGetHitlDetails = <TData = Common.TaskInstanceServiceGetHitlDetailsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { bodySearch?: string; + createdAtGt?: string; + createdAtGte?: string; + createdAtLt?: string; + createdAtLte?: string; dagId: string; dagIdPattern?: string; dagRunId: string; @@ -1101,7 +1109,7 @@ export const useTaskInstanceServiceGetHitlDetails = <TData = Common.TaskInstance subjectSearch?: string; taskId?: string; taskIdPattern?: string; -}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }, queryKey), queryFn: () => TaskInstanceService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, res [...] +}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }, queryKey), queryFn: () => TaskInstanceService.getHitlDetails({ bodySearch, crea [...] /** * Get Import Error * Get an import error. diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts index 4d5fea56dac..61cb9a59f07 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts @@ -1083,11 +1083,19 @@ export const useTaskInstanceServiceGetHitlDetailSuspense = <TData = Common.TaskI * @param data.respondedByUserName * @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.createdAtGte +* @param data.createdAtGt +* @param data.createdAtLte +* @param data.createdAtLt * @returns HITLDetailCollection Successful Response * @throws ApiError */ -export const useTaskInstanceServiceGetHitlDetailsSuspense = <TData = Common.TaskInstanceServiceGetHitlDetailsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { +export const useTaskInstanceServiceGetHitlDetailsSuspense = <TData = Common.TaskInstanceServiceGetHitlDetailsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { bodySearch?: string; + createdAtGt?: string; + createdAtGte?: string; + createdAtLt?: string; + createdAtLte?: string; dagId: string; dagIdPattern?: string; dagRunId: string; @@ -1101,7 +1109,7 @@ export const useTaskInstanceServiceGetHitlDetailsSuspense = <TData = Common.Task subjectSearch?: string; taskId?: string; taskIdPattern?: string; -}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }, queryKey), queryFn: () => TaskInstanceService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orde [...] +}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }, queryKey), queryFn: () => TaskInstanceService.getHitlDetails({ bodySear [...] /** * Get Import Error * Get an import error. diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts index 1cf2a46b461..32dafcb90fd 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -3578,6 +3578,11 @@ export const $HITLDetail = { type: 'array', title: 'Assigned Users' }, + created_at: { + type: 'string', + format: 'date-time', + title: 'Created At' + }, responded_by_user: { anyOf: [ { @@ -3626,7 +3631,7 @@ export const $HITLDetail = { } }, type: 'object', - required: ['task_instance', 'options', 'subject'], + required: ['task_instance', 'options', 'subject', 'created_at'], title: 'HITLDetail', description: 'Schema for Human-in-the-loop detail.' } as const; diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts index 468bea4460d..9cd340c85a0 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts @@ -2792,6 +2792,10 @@ export class TaskInstanceService { * @param data.respondedByUserName * @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + * @param data.createdAtGte + * @param data.createdAtGt + * @param data.createdAtLte + * @param data.createdAtLt * @returns HITLDetailCollection Successful Response * @throws ApiError */ @@ -2815,7 +2819,11 @@ export class TaskInstanceService { responded_by_user_id: data.respondedByUserId, responded_by_user_name: data.respondedByUserName, subject_search: data.subjectSearch, - body_search: data.bodySearch + body_search: data.bodySearch, + created_at_gte: data.createdAtGte, + created_at_gt: data.createdAtGt, + created_at_lte: data.createdAtLte, + created_at_lt: data.createdAtLt }, errors: { 401: 'Unauthorized', diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts index 18d40ed2531..5ee9f5edf90 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts @@ -941,6 +941,7 @@ export type HITLDetail = { [key: string]: unknown; }; assigned_users?: Array<HITLUser>; + created_at: string; responded_by_user?: HITLUser | null; responded_at?: string | null; chosen_options?: Array<(string)> | null; @@ -2851,6 +2852,10 @@ export type GetHitlDetailsData = { * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. */ bodySearch?: string | null; + createdAtGt?: string | null; + createdAtGte?: string | null; + createdAtLt?: string | null; + createdAtLte?: string | null; dagId: string; /** * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_hitl.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_hitl.py index 61f6488d5fc..f8ab8e1c4d9 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_hitl.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_hitl.py @@ -18,7 +18,7 @@ from __future__ import annotations import json from collections.abc import Callable -from datetime import datetime +from datetime import datetime, timedelta from operator import itemgetter from typing import TYPE_CHECKING, Any from unittest import mock @@ -28,12 +28,14 @@ import time_machine from sqlalchemy import select from sqlalchemy.orm import Session -from airflow._shared.timezones.timezone import utcnow +from airflow._shared.timezones.timezone import utc, utcnow from airflow.models.hitl import HITLDetail from airflow.models.log import Log from airflow.sdk.execution_time.hitl import HITLUser from airflow.utils.state import TaskInstanceState +from tests_common.test_utils.format_datetime import from_datetime_to_zulu_without_ms + if TYPE_CHECKING: from fastapi.testclient import TestClient @@ -49,6 +51,10 @@ ANOTHER_DAG_ID = "another_hitl_dag" TASK_ID = "sample_task_hitl" +DEFAULT_CREATED_AT = datetime(2025, 9, 15, 13, 0, 0, tzinfo=utc) +ANOTHER_CREATED_AT = datetime(2025, 9, 16, 12, 0, 0, tzinfo=utc) + + @pytest.fixture def sample_ti( create_task_instance: CreateTaskInstance, @@ -158,6 +164,7 @@ def sample_hitl_details(sample_tis: list[TaskInstance], session: Session) -> lis defaults=["Approve"], multiple=False, params={"input_1": 1}, + created_at=DEFAULT_CREATED_AT, ) for i, ti in enumerate(sample_tis[:5]) ] @@ -175,6 +182,7 @@ def sample_hitl_details(sample_tis: list[TaskInstance], session: Session) -> lis chosen_options=[str(i)], params_input={"input": i}, responded_by={"id": "test", "name": "test"}, + created_at=ANOTHER_CREATED_AT, ) for i, ti in enumerate(sample_tis[5:]) ] @@ -209,6 +217,7 @@ def expected_sample_hitl_detail_dict(sample_ti: TaskInstance) -> dict[str, Any]: "options": ["Approve", "Reject"], "params": {"input_1": 1}, "assigned_users": [], + "created_at": mock.ANY, "params_input": {}, "responded_at": None, "chosen_options": None, @@ -542,6 +551,21 @@ class TestGetHITLDetailsEndpoint: ({"response_received": True}, 3), ({"responded_by_user_id": ["test"]}, 3), ({"responded_by_user_name": ["test"]}, 3), + ( + {"created_at_gte": from_datetime_to_zulu_without_ms(DEFAULT_CREATED_AT + timedelta(days=1))}, + 0, + ), + ( + {"created_at_lte": from_datetime_to_zulu_without_ms(DEFAULT_CREATED_AT - timedelta(days=1))}, + 0, + ), + ( + { + "created_at_gte": from_datetime_to_zulu_without_ms(DEFAULT_CREATED_AT), + "created_at_lte": from_datetime_to_zulu_without_ms(DEFAULT_CREATED_AT), + }, + 5, + ), ], ids=[ "dag_id_pattern_hitl_dag", @@ -556,6 +580,9 @@ class TestGetHITLDetailsEndpoint: "response_received", "responded_user_id", "responded_user_name", + "created_at_gte", + "created_at_lte", + "created_at", ], ) def test_should_respond_200_with_existing_response_and_query( @@ -587,6 +614,7 @@ class TestGetHITLDetailsEndpoint: "multiple": False, "params": {"input_1": 1}, "assigned_users": [], + "created_at": DEFAULT_CREATED_AT.isoformat().replace("+00:00", "Z"), "responded_by_user": None, "responded_at": None, "chosen_options": None, @@ -612,6 +640,7 @@ class TestGetHITLDetailsEndpoint: # htil key ("subject", itemgetter("subject")), ("responded_at", itemgetter("responded_at")), + ("created_at", itemgetter("created_at")), ], ids=[ "ti_id", @@ -622,6 +651,7 @@ class TestGetHITLDetailsEndpoint: "task_instance_operator", "subject", "responded_at", + "created_at", ], ) def test_should_respond_200_with_existing_response_and_order_by( diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py index d58f29839b6..5ebf2221d92 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py @@ -188,6 +188,7 @@ class TestGetDagRuns(TestPublicDagEndpoint): "params_input": {}, "response_received": False, "assigned_users": [], + "created_at": mock.ANY, } for i in range(3) ], diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py b/airflow-ctl/src/airflowctl/api/datamodels/generated.py index 31da4e13044..739297fd434 100644 --- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py +++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py @@ -1837,6 +1837,7 @@ class HITLDetail(BaseModel): multiple: Annotated[bool | None, Field(title="Multiple")] = False params: Annotated[dict[str, Any] | None, Field(title="Params")] = None assigned_users: Annotated[list[HITLUser] | None, Field(title="Assigned Users")] = None + created_at: Annotated[datetime, Field(title="Created At")] responded_by_user: HITLUser | None = None responded_at: Annotated[datetime | None, Field(title="Responded At")] = None chosen_options: Annotated[list[str] | None, Field(title="Chosen Options")] = None
