You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by ka...@apache.org on 2025/05/06 13:25:11 UTC

(airflow) branch v3-0-test updated (f558eadcfe4 -> 3ecefff6e93)

This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a change to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


    from f558eadcfe4 [v3-0-test] Render custom `map_index_template` on task completion (#49809) (#50194)
     new 43aaec81271 docs(ruff): fix outdated content and add airflow rule list, ruff configuration links (#50232)
     new f2cae5f3c4c Fix ContinuousTimetable false triggering when last run ends in future (#45175)
     new 93960d8d194 Add `dag_run_conf` to `RunBackfillForm` (#49763)
     new 9a43eb8d736 minor cosmetic changes (#50044)
     new 4cc590d559e refactor: migrate to new ui endpoint (#50028)
     new b9d5264a654 Refactor Dashboard to enhance layout (#50026)
     new 733c7ee97ea Added the download button on the assets page (#50045)
     new b174882b9da Add dateInterval validation and error handling (#50072)
     new e23a474437f Bump vite from 5.4.18 to 5.4.19 in /airflow-core/src/airflow/ui (#50074)
     new 848c1b31140 Added validation in flexibleform (#49981)
     new d5812660b1e Restore hover+selection crosshairs to grid view (#50050)
     new 6c42950f543 Fix date range field alignment (#50086)
     new e3b068ddb30 Add auto-refresh for `Stats` (#50088)
     new 8490b2c3ca3 Fix node selections (#50095)
     new a7e74d33fc4 Fix duration charts (#50094)
     new ca8d0114cd1 Fix datetime validation (#50116)
     new cae60348d19 Add TIs tab to mapped task details (#50085)
     new 44bb1010372 Added focus view on grid and graph on second click (#50125)
     new 5ad8cb83b37 Fixes validation error and adds error indicator (#50127)
     new 604e65d76f9 Add formatted extra to asset events (#50124)
     new 554e15921ba fix: wrap overflowing texts of asset events (#50173)
     new a40c476b4ab Add audit log extra to table and improve UX (#50100)
     new 746257fc105 Use top-level imports in docs if possible (#50195)
     new f001dc2d9b2 Handle MappedTaskGroup map indexes (#49996)
     new 3ecefff6e93 Move webserver expose config to api section (#50209)

The 25 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 Dockerfile.ci                                      |   2 +-
 .../docs/authoring-and-scheduling/assets.rst       |   6 +-
 airflow-core/docs/best-practices.rst               |  10 +-
 .../docs/installation/upgrading_to_airflow3.rst    |   3 +
 .../api_fastapi/core_api/routes/public/config.py   |   4 +-
 .../execution_api/datamodels/taskinstance.py       |   2 +-
 .../execution_api/routes/task_instances.py         |  40 ++++-
 .../src/airflow/cli/commands/config_command.py     |   4 +
 .../src/airflow/config_templates/config.yml        |  18 +--
 airflow-core/src/airflow/configuration.py          |   3 +-
 airflow-core/src/airflow/timetables/simple.py      |  17 +-
 airflow-core/src/airflow/ui/package.json           |   2 +-
 airflow-core/src/airflow/ui/pnpm-lock.yaml         |  50 ++++--
 .../ui/src/components/Assets/AssetEvent.tsx        |  32 ++--
 .../ui/src/components/Assets/AssetEvents.tsx       |   8 +-
 .../src/airflow/ui/src/components/ConfigForm.tsx   | 121 ++++++++++++++
 .../src/components/DagActions/RunBackfillForm.tsx  |  93 ++++++-----
 .../ui/src/components/DataTable/DataTable.tsx      |   6 +-
 .../airflow/ui/src/components/DateTimeInput.tsx    |   4 +-
 .../airflow/ui/src/components/DurationChart.tsx    |   4 +-
 .../components/FlexibleForm/FieldAdvancedArray.tsx |  27 +---
 .../src/components/FlexibleForm/FieldDateTime.tsx  |   4 +-
 .../src/components/FlexibleForm/FieldDropdown.tsx  |   3 +-
 .../components/FlexibleForm/FieldMultiSelect.tsx   |   3 +-
 .../components/FlexibleForm/FieldMultilineText.tsx |   3 +-
 .../ui/src/components/FlexibleForm/FieldNumber.tsx |   3 +-
 .../ui/src/components/FlexibleForm/FieldObject.tsx |  23 +--
 .../ui/src/components/FlexibleForm/FieldRow.tsx    |  38 +++--
 .../src/components/FlexibleForm/FieldSelector.tsx  |  26 +--
 .../ui/src/components/FlexibleForm/FieldString.tsx |   3 +-
 .../components/FlexibleForm/FieldStringArray.tsx   |   3 +-
 .../src/components/FlexibleForm/FlexibleForm.tsx   |  56 ++++++-
 .../airflow/ui/src/components/FlexibleForm/Row.tsx |   8 +-
 .../ui/src/components/FlexibleForm/index.tsx       |   1 +
 .../components/FlexibleForm/isParamRequired.tsx}   |  16 +-
 .../airflow/ui/src/components/Graph/DagNode.tsx    |   3 +-
 .../ui/src/components/Graph/DefaultNode.tsx        |   1 +
 .../ui/src/components/Graph/DownloadButton.tsx     |   4 +-
 .../ui/src/components/RenderedJsonField.tsx        |   2 +-
 .../src/components/TriggerDag/TriggerDAGForm.tsx   | 178 +++++++--------------
 .../ui/src/layouts/Details/DetailsLayout.tsx       |  12 +-
 .../airflow/ui/src/layouts/Details/Graph/Graph.tsx |   4 +-
 .../airflow/ui/src/layouts/Details/Grid/Grid.tsx   |   2 +-
 .../airflow/ui/src/layouts/Details/Grid/GridTI.tsx | 105 ++++++------
 .../ui/src/layouts/Details/Grid/TaskNames.tsx      |  22 ++-
 .../ui/src/layouts/Details/PanelButtons.tsx        |  35 +++-
 .../src/airflow/ui/src/pages/Asset/Asset.tsx       |   1 -
 .../src/airflow/ui/src/pages/Asset/AssetGraph.tsx  |   4 +-
 .../ui/src/pages/Connections/ConnectionForm.tsx    |   5 +-
 .../airflow/ui/src/pages/Dashboard/Dashboard.tsx   |   2 +-
 .../Dashboard/HistoricalMetrics/DagRunMetrics.tsx  |  31 ++--
 .../Dashboard/HistoricalMetrics/MetricSection.tsx  |   2 +-
 .../HistoricalMetrics/TaskInstanceMetrics.tsx      |  39 ++---
 .../airflow/ui/src/pages/Dashboard/Stats/Stats.tsx |  37 ++---
 .../src/airflow/ui/src/pages/Events/Events.tsx     | 105 ++++++++----
 .../MappedTaskInstance/MappedTaskInstance.tsx      |   6 +-
 .../ui/src/pages/TaskInstance/TaskInstance.tsx     |  43 ++++-
 .../auth/managers/simple/test_middleware.py        |   9 +-
 .../core_api/routes/public/test_config.py          |   6 +-
 .../versions/head/test_task_instances.py           |   3 +
 .../unit/timetables/test_continuous_timetable.py   |  11 ++
 chart/docs/airflow-configuration.rst               |   2 +-
 clients/python/README.md                           |   4 +-
 dev/README_RELEASE_PYTHON_CLIENT.md                |   4 +-
 devel-common/src/tests_common/pytest_plugin.py     |   3 +-
 scripts/docker/entrypoint_ci.sh                    |   2 +-
 .../src/airflow/sdk/api/datamodels/_generated.py   |   4 +-
 task-sdk/src/airflow/sdk/definitions/xcom_arg.py   |  23 ++-
 .../task_sdk/definitions/test_mappedoperator.py    |  15 +-
 69 files changed, 879 insertions(+), 496 deletions(-)
 create mode 100644 airflow-core/src/airflow/ui/src/components/ConfigForm.tsx
 copy airflow-core/src/airflow/{api_fastapi/auth/managers/simple/ui/rules/off.js => ui/src/components/FlexibleForm/isParamRequired.tsx} (66%)


(airflow) 09/25: Bump vite from 5.4.18 to 5.4.19 in /airflow-core/src/airflow/ui (#50074)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit e23a474437f08901d9354115e7e576b835b9dbfb
Author: Dependabot [bot] <49...@users.noreply.github.com>
AuthorDate: Thu May 1 10:53:37 2025 -0400

    Bump vite from 5.4.18 to 5.4.19 in /airflow-core/src/airflow/ui (#50074)
    
    Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.18 to 5.4.19.
    - [Release notes](https://github.com/vitejs/vite/releases)
    - [Changelog](https://github.com/vitejs/vite/blob/v5.4.19/packages/vite/CHANGELOG.md)
    - [Commits](https://github.com/vitejs/vite/commits/v5.4.19/packages/vite)
    
    ---
    updated-dependencies:
    - dependency-name: vite
      dependency-version: 5.4.19
      dependency-type: direct:development
    ...
    
    Signed-off-by: dependabot[bot] <su...@github.com>
    Co-authored-by: dependabot[bot] <49...@users.noreply.github.com>
    (cherry picked from commit eb155fb00e9482918e6cf14a226365a31c1c53fd)
---
 airflow-core/src/airflow/ui/package.json   |  2 +-
 airflow-core/src/airflow/ui/pnpm-lock.yaml | 50 ++++++++++++++++++++----------
 2 files changed, 34 insertions(+), 18 deletions(-)

diff --git a/airflow-core/src/airflow/ui/package.json b/airflow-core/src/airflow/ui/package.json
index 13840ca2105..ba22d9c745a 100644
--- a/airflow-core/src/airflow/ui/package.json
+++ b/airflow-core/src/airflow/ui/package.json
@@ -85,7 +85,7 @@
     "prettier": "^3.5.3",
     "typescript": "~5.5.4",
     "typescript-eslint": "^8.27.0",
-    "vite": "^5.4.18",
+    "vite": "^5.4.19",
     "vite-plugin-css-injected-by-js": "^3.5.2",
     "vitest": "^2.1.9",
     "web-worker": "^1.5.0"
diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml b/airflow-core/src/airflow/ui/pnpm-lock.yaml
index b52e4d22619..3b90be0585b 100644
--- a/airflow-core/src/airflow/ui/pnpm-lock.yaml
+++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml
@@ -158,7 +158,7 @@ importers:
         version: 15.5.13
       '@vitejs/plugin-react-swc':
         specifier: ^3.8.1
-        version: 3.8.1(@swc/helpers@0.5.15)(vite@5.4.18(@types/node@22.13.11))
+        version: 3.8.1(@swc/helpers@0.5.15)(vite@5.4.19(@types/node@22.13.11))
       '@vitest/coverage-v8':
         specifier: ^2.1.9
         version: 2.1.9(vitest@2.1.9(@types/node@22.13.11)(happy-dom@17.4.4)(msw@2.7.3(@types/node@22.13.11)(typescript@5.5.4)))
@@ -211,11 +211,11 @@ importers:
         specifier: ^8.27.0
         version: 8.27.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.5.4)
       vite:
-        specifier: ^5.4.18
-        version: 5.4.18(@types/node@22.13.11)
+        specifier: ^5.4.19
+        version: 5.4.19(@types/node@22.13.11)
       vite-plugin-css-injected-by-js:
         specifier: ^3.5.2
-        version: 3.5.2(vite@5.4.18(@types/node@22.13.11))
+        version: 3.5.2(vite@5.4.19(@types/node@22.13.11))
       vitest:
         specifier: ^2.1.9
         version: 2.1.9(@types/node@22.13.11)(happy-dom@17.4.4)(msw@2.7.3(@types/node@22.13.11)(typescript@5.5.4))
@@ -256,6 +256,10 @@ packages:
     resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/code-frame@7.27.1':
+    resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/generator@7.17.7':
     resolution: {integrity: sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==}
     engines: {node: '>=6.9.0'}
@@ -292,6 +296,10 @@ packages:
     resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/helper-validator-identifier@7.27.1':
+    resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/parser@7.26.10':
     resolution: {integrity: sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==}
     engines: {node: '>=6.0.0'}
@@ -4017,8 +4025,8 @@ packages:
     peerDependencies:
       vite: '>2.0.0-0'
 
-  vite@5.4.18:
-    resolution: {integrity: sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==}
+  vite@5.4.19:
+    resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
     peerDependencies:
@@ -4298,6 +4306,12 @@ snapshots:
       js-tokens: 4.0.0
       picocolors: 1.1.1
 
+  '@babel/code-frame@7.27.1':
+    dependencies:
+      '@babel/helper-validator-identifier': 7.27.1
+      js-tokens: 4.0.0
+      picocolors: 1.1.1
+
   '@babel/generator@7.17.7':
     dependencies:
       '@babel/types': 7.17.0
@@ -4340,6 +4354,8 @@ snapshots:
 
   '@babel/helper-validator-identifier@7.25.9': {}
 
+  '@babel/helper-validator-identifier@7.27.1': {}
+
   '@babel/parser@7.26.10':
     dependencies:
       '@babel/types': 7.26.10
@@ -4983,7 +4999,7 @@ snapshots:
 
   '@testing-library/dom@10.4.0':
     dependencies:
-      '@babel/code-frame': 7.26.2
+      '@babel/code-frame': 7.27.1
       '@babel/runtime': 7.26.10
       '@types/aria-query': 5.0.4
       aria-query: 5.3.0
@@ -5681,10 +5697,10 @@ snapshots:
       d3-time-format: 4.1.0
       internmap: 2.0.3
 
-  '@vitejs/plugin-react-swc@3.8.1(@swc/helpers@0.5.15)(vite@5.4.18(@types/node@22.13.11))':
+  '@vitejs/plugin-react-swc@3.8.1(@swc/helpers@0.5.15)(vite@5.4.19(@types/node@22.13.11))':
     dependencies:
       '@swc/core': 1.11.12(@swc/helpers@0.5.15)
-      vite: 5.4.18(@types/node@22.13.11)
+      vite: 5.4.19(@types/node@22.13.11)
     transitivePeerDependencies:
       - '@swc/helpers'
 
@@ -5713,14 +5729,14 @@ snapshots:
       chai: 5.2.0
       tinyrainbow: 1.2.0
 
-  '@vitest/mocker@2.1.9(msw@2.7.3(@types/node@22.13.11)(typescript@5.5.4))(vite@5.4.18(@types/node@22.13.11))':
+  '@vitest/mocker@2.1.9(msw@2.7.3(@types/node@22.13.11)(typescript@5.5.4))(vite@5.4.19(@types/node@22.13.11))':
     dependencies:
       '@vitest/spy': 2.1.9
       estree-walker: 3.0.3
       magic-string: 0.30.17
     optionalDependencies:
       msw: 2.7.3(@types/node@22.13.11)(typescript@5.5.4)
-      vite: 5.4.18(@types/node@22.13.11)
+      vite: 5.4.19(@types/node@22.13.11)
 
   '@vitest/pretty-format@2.1.9':
     dependencies:
@@ -9158,7 +9174,7 @@ snapshots:
       debug: 4.4.0
       es-module-lexer: 1.6.0
       pathe: 1.1.2
-      vite: 5.4.18(@types/node@22.13.11)
+      vite: 5.4.19(@types/node@22.13.11)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -9170,11 +9186,11 @@ snapshots:
       - supports-color
       - terser
 
-  vite-plugin-css-injected-by-js@3.5.2(vite@5.4.18(@types/node@22.13.11)):
+  vite-plugin-css-injected-by-js@3.5.2(vite@5.4.19(@types/node@22.13.11)):
     dependencies:
-      vite: 5.4.18(@types/node@22.13.11)
+      vite: 5.4.19(@types/node@22.13.11)
 
-  vite@5.4.18(@types/node@22.13.11):
+  vite@5.4.19(@types/node@22.13.11):
     dependencies:
       esbuild: 0.21.5
       postcss: 8.5.3
@@ -9186,7 +9202,7 @@ snapshots:
   vitest@2.1.9(@types/node@22.13.11)(happy-dom@17.4.4)(msw@2.7.3(@types/node@22.13.11)(typescript@5.5.4)):
     dependencies:
       '@vitest/expect': 2.1.9
-      '@vitest/mocker': 2.1.9(msw@2.7.3(@types/node@22.13.11)(typescript@5.5.4))(vite@5.4.18(@types/node@22.13.11))
+      '@vitest/mocker': 2.1.9(msw@2.7.3(@types/node@22.13.11)(typescript@5.5.4))(vite@5.4.19(@types/node@22.13.11))
       '@vitest/pretty-format': 2.1.9
       '@vitest/runner': 2.1.9
       '@vitest/snapshot': 2.1.9
@@ -9202,7 +9218,7 @@ snapshots:
       tinyexec: 0.3.2
       tinypool: 1.0.2
       tinyrainbow: 1.2.0
-      vite: 5.4.18(@types/node@22.13.11)
+      vite: 5.4.19(@types/node@22.13.11)
       vite-node: 2.1.9(@types/node@22.13.11)
       why-is-node-running: 2.3.0
     optionalDependencies:


(airflow) 20/25: Add formatted extra to asset events (#50124)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 604e65d76f96006c252b08e5f8537dcab96ee1ba
Author: Brent Bovenzi <br...@astronomer.io>
AuthorDate: Sat May 3 04:24:44 2025 -0400

    Add formatted extra to asset events (#50124)
    
    * Always show asset event extra, as json
    
    * Change clipboard icon size
    
    (cherry picked from commit 50f644d25ce7cf9bf81d12612c2408699807c6d6)
---
 .../airflow/ui/src/components/Assets/AssetEvent.tsx    | 18 ++++++++----------
 .../airflow/ui/src/components/Assets/AssetEvents.tsx   |  8 +++-----
 .../airflow/ui/src/components/RenderedJsonField.tsx    |  2 +-
 airflow-core/src/airflow/ui/src/pages/Asset/Asset.tsx  |  1 -
 4 files changed, 12 insertions(+), 17 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/Assets/AssetEvent.tsx b/airflow-core/src/airflow/ui/src/components/Assets/AssetEvent.tsx
index 6c97c648de5..1cb0817964e 100644
--- a/airflow-core/src/airflow/ui/src/components/Assets/AssetEvent.tsx
+++ b/airflow-core/src/airflow/ui/src/components/Assets/AssetEvent.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Text, HStack, Code } from "@chakra-ui/react";
+import { Box, Text, HStack } from "@chakra-ui/react";
 import { FiDatabase } from "react-icons/fi";
 import { Link } from "react-router-dom";
 
@@ -24,30 +24,26 @@ import type { AssetEventResponse } from "openapi/requests/types.gen";
 import Time from "src/components/Time";
 import { Tooltip } from "src/components/ui";
 
+import RenderedJsonField from "../RenderedJsonField";
 import { TriggeredRuns } from "./TriggeredRuns";
 
 export const AssetEvent = ({
   assetId,
   event,
-  showExtra,
 }: {
   readonly assetId?: number;
   readonly event: AssetEventResponse;
-  readonly showExtra?: boolean;
 }) => {
   let source = "";
 
-  // eslint-disable-next-line @typescript-eslint/naming-convention
-  const { from_rest_api, from_trigger, ...extra } = event.extra ?? {};
+  const { from_rest_api: fromRestAPI, from_trigger: fromTrigger, ...extra } = event.extra ?? {};
 
-  if (from_rest_api === true) {
+  if (fromRestAPI === true) {
     source = "API";
-  } else if (from_trigger === true) {
+  } else if (fromTrigger === true) {
     source = "Trigger";
   }
 
-  const extraString = JSON.stringify(extra);
-
   return (
     <Box borderBottomWidth={1} fontSize={13} mt={1} p={2}>
       <Text fontWeight="bold">
@@ -86,7 +82,9 @@ export const AssetEvent = ({
       <HStack>
         <TriggeredRuns dagRuns={event.created_dagruns} />
       </HStack>
-      {showExtra && extraString !== "{}" ? <Code>{extraString}</Code> : undefined}
+      {Object.keys(extra).length >= 1 ? (
+        <RenderedJsonField content={extra} jsonProps={{ collapsed: true }} />
+      ) : undefined}
     </Box>
   );
 };
diff --git a/airflow-core/src/airflow/ui/src/components/Assets/AssetEvents.tsx b/airflow-core/src/airflow/ui/src/components/Assets/AssetEvents.tsx
index f06f55185c9..4becd16cc95 100644
--- a/airflow-core/src/airflow/ui/src/components/Assets/AssetEvents.tsx
+++ b/airflow-core/src/airflow/ui/src/components/Assets/AssetEvents.tsx
@@ -29,8 +29,8 @@ import { DataTable } from "../DataTable";
 import type { CardDef, TableState } from "../DataTable/types";
 import { AssetEvent } from "./AssetEvent";
 
-const cardDef = (assetId?: number, showExtra?: boolean): CardDef<AssetEventResponse> => ({
-  card: ({ row }) => <AssetEvent assetId={assetId} event={row} showExtra={showExtra} />,
+const cardDef = (assetId?: number): CardDef<AssetEventResponse> => ({
+  card: ({ row }) => <AssetEvent assetId={assetId} event={row} />,
   meta: {
     customSkeleton: <Skeleton height="120px" width="100%" />,
   },
@@ -42,7 +42,6 @@ type AssetEventProps = {
   readonly isLoading?: boolean;
   readonly setOrderBy?: (order: string) => void;
   readonly setTableUrlState?: (state: TableState) => void;
-  readonly showExtra?: boolean;
   readonly tableUrlState?: TableState;
   readonly title?: string;
 };
@@ -53,7 +52,6 @@ export const AssetEvents = ({
   isLoading,
   setOrderBy,
   setTableUrlState,
-  showExtra,
   tableUrlState,
   title,
 }: AssetEventProps) => {
@@ -100,7 +98,7 @@ export const AssetEvents = ({
         )}
       </Flex>
       <DataTable
-        cardDef={cardDef(assetId, showExtra)}
+        cardDef={cardDef(assetId)}
         columns={[]}
         data={data?.asset_events ?? []}
         displayMode="card"
diff --git a/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx b/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx
index 132a57116a6..5e32d8c67d7 100644
--- a/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx
+++ b/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx
@@ -47,7 +47,7 @@ const RenderedJsonField = ({ content, jsonProps, ...rest }: Props) => {
         {...jsonProps}
       />
       <ClipboardRoot value={contentFormatted}>
-        <ClipboardIconButton />
+        <ClipboardIconButton h={7} minW={7} />
       </ClipboardRoot>
     </Flex>
   );
diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/Asset.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/Asset.tsx
index 9f8ff455504..0a961d19d2c 100644
--- a/airflow-core/src/airflow/ui/src/pages/Asset/Asset.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Asset/Asset.tsx
@@ -109,7 +109,6 @@ export const Asset = () => {
                 isLoading={isLoadingEvents}
                 setOrderBy={setOrderBy}
                 setTableUrlState={setTableURLState}
-                showExtra
                 tableUrlState={tableURLState}
               />
             </Box>


(airflow) 25/25: Move webserver expose config to api section (#50209)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 3ecefff6e9370fafe856eb5183f0e74657641436
Author: Pierre Jeambrun <pi...@gmail.com>
AuthorDate: Tue May 6 11:05:35 2025 +0200

    Move webserver expose config to api section (#50209)
    
    (cherry picked from commit 31c7c9626bab3e39a5a0592b87e220007d94d6b9)
---
 Dockerfile.ci                                          |  2 +-
 .../api_fastapi/core_api/routes/public/config.py       |  4 ++--
 .../src/airflow/cli/commands/config_command.py         |  4 ++++
 airflow-core/src/airflow/config_templates/config.yml   | 18 +++++++++---------
 airflow-core/src/airflow/configuration.py              |  3 ++-
 .../auth/managers/simple/test_middleware.py            |  9 +++++----
 .../api_fastapi/core_api/routes/public/test_config.py  |  6 +++---
 chart/docs/airflow-configuration.rst                   |  2 +-
 clients/python/README.md                               |  4 ++--
 dev/README_RELEASE_PYTHON_CLIENT.md                    |  4 ++--
 scripts/docker/entrypoint_ci.sh                        |  2 +-
 11 files changed, 32 insertions(+), 26 deletions(-)

diff --git a/Dockerfile.ci b/Dockerfile.ci
index ac686c84ecc..39a5373ee1c 100644
--- a/Dockerfile.ci
+++ b/Dockerfile.ci
@@ -1121,7 +1121,7 @@ function start_api_server_with_examples(){
         return
     fi
     export AIRFLOW__CORE__LOAD_EXAMPLES=True
-    export AIRFLOW__WEBSERVER__EXPOSE_CONFIG=True
+    export AIRFLOW__API__EXPOSE_CONFIG=True
     echo
     echo "${COLOR_BLUE}Initializing database${COLOR_RESET}"
     echo
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/config.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/config.py
index 1df15825915..1509dd25e6f 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/config.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/config.py
@@ -70,11 +70,11 @@ text_example_response_for_get_config = {
 
 def _check_expose_config() -> bool:
     display_sensitive: bool | None = None
-    if conf.get("webserver", "expose_config").lower() == "non-sensitive-only":
+    if conf.get("api", "expose_config").lower() == "non-sensitive-only":
         expose_config = True
         display_sensitive = False
     else:
-        expose_config = conf.getboolean("webserver", "expose_config")
+        expose_config = conf.getboolean("api", "expose_config")
         display_sensitive = True
 
     if not expose_config:
diff --git a/airflow-core/src/airflow/cli/commands/config_command.py b/airflow-core/src/airflow/cli/commands/config_command.py
index b953e61d09c..0f5c5df5ba1 100644
--- a/airflow-core/src/airflow/cli/commands/config_command.py
+++ b/airflow-core/src/airflow/cli/commands/config_command.py
@@ -495,6 +495,10 @@ CONFIGS_CHANGES = [
         renamed_to=ConfigParameter("fab", "proxy_fix_x_prefix"),
         breaking=True,
     ),
+    ConfigChange(
+        config=ConfigParameter("webserver", "expose_config"),
+        renamed_to=ConfigParameter("api", "expose_config"),
+    ),
     ConfigChange(
         config=ConfigParameter("webserver", "cookie_secure"),
         was_deprecated=False,
diff --git a/airflow-core/src/airflow/config_templates/config.yml b/airflow-core/src/airflow/config_templates/config.yml
index f6824e3da38..50a849a099d 100644
--- a/airflow-core/src/airflow/config_templates/config.yml
+++ b/airflow-core/src/airflow/config_templates/config.yml
@@ -1318,6 +1318,15 @@ secrets:
 api:
   description: ~
   options:
+    expose_config:
+      description: |
+        Expose the configuration file in the web server. Set to ``non-sensitive-only`` to show all values
+        except those that have security implications. ``True`` shows all values. ``False`` hides the
+        configuration completely.
+      version_added: ~
+      type: string
+      example: ~
+      default: "False"
     base_url:
       description: |
         The base url of the API server. Airflow cannot guess what domain or CNAME you are using.
@@ -1739,15 +1748,6 @@ webserver:
       sensitive: true
       example: ~
       default: "{SECRET_KEY}"
-    expose_config:
-      description: |
-        Expose the configuration file in the web server. Set to ``non-sensitive-only`` to show all values
-        except those that have security implications. ``True`` shows all values. ``False`` hides the
-        configuration completely.
-      version_added: ~
-      type: string
-      example: ~
-      default: "False"
     expose_hostname:
       description: |
         Expose hostname in the web server
diff --git a/airflow-core/src/airflow/configuration.py b/airflow-core/src/airflow/configuration.py
index c48377c333c..ff214b3138a 100644
--- a/airflow-core/src/airflow/configuration.py
+++ b/airflow-core/src/airflow/configuration.py
@@ -355,6 +355,7 @@ class AirflowConfigParser(ConfigParser):
         ("api", "ssl_key"): ("webserver", "web_server_ssl_key", "3.0"),
         ("api", "access_logfile"): ("webserver", "access_logfile", "3.0"),
         ("triggerer", "capacity"): ("triggerer", "default_capacity", "3.0"),
+        ("api", "expose_config"): ("webserver", "expose_config", "3.0.1"),
     }
 
     # A mapping of new section -> (old section, since_version).
@@ -1294,7 +1295,7 @@ class AirflowConfigParser(ConfigParser):
 
     def read(
         self,
-        filenames: (str | bytes | os.PathLike | Iterable[str | bytes | os.PathLike]),
+        filenames: str | bytes | os.PathLike | Iterable[str | bytes | os.PathLike],
         encoding=None,
     ):
         super().read(filenames=filenames, encoding=encoding)
diff --git a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py
index dcd39ef19ed..fa4bd5566b7 100644
--- a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py
+++ b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py
@@ -32,7 +32,7 @@ def all_access_test_client():
     with conf_vars(
         {
             ("core", "simple_auth_manager_all_admins"): "true",
-            ("webserver", "expose_config"): "true",
+            ("api", "expose_config"): "true",
         }
     ):
         app = create_app()
@@ -56,6 +56,7 @@ def all_access_test_client():
 )
 def test_all_endpoints_without_auth_header(all_access_test_client, method, path):
     response = all_access_test_client.request(method, path)
-    assert response.status_code not in {401, 403}, (
-        f"Unexpected status code {response.status_code} for {method} {path}"
-    )
+    assert response.status_code not in {
+        401,
+        403,
+    }, f"Unexpected status code {response.status_code} for {method} {path}"
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_config.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_config.py
index 1fec270f34f..c057d2d32a7 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_config.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_config.py
@@ -77,9 +77,9 @@ MOCK_CONFIG_OVERRIDE = {
     (SECTION_SMTP, OPTION_KEY_SMTP_MAIL_FROM): OPTION_VALUE_SMTP_MAIL_FROM,
 }
 
-AIRFLOW_CONFIG_ENABLE_EXPOSE_CONFIG = {("webserver", "expose_config"): "True"}
-AIRFLOW_CONFIG_DISABLE_EXPOSE_CONFIG = {("webserver", "expose_config"): "False"}
-AIRFLOW_CONFIG_NON_SENSITIVE_ONLY_CONFIG = {("webserver", "expose_config"): "non-sensitive-only"}
+AIRFLOW_CONFIG_ENABLE_EXPOSE_CONFIG = {("api", "expose_config"): "True"}
+AIRFLOW_CONFIG_DISABLE_EXPOSE_CONFIG = {("api", "expose_config"): "False"}
+AIRFLOW_CONFIG_NON_SENSITIVE_ONLY_CONFIG = {("api", "expose_config"): "non-sensitive-only"}
 FORBIDDEN_RESPONSE = {
     "detail": "Your Airflow administrator chose not to expose the configuration, most likely for security reasons."
 }
diff --git a/chart/docs/airflow-configuration.rst b/chart/docs/airflow-configuration.rst
index 39a541fb505..65c7cb83e3f 100644
--- a/chart/docs/airflow-configuration.rst
+++ b/chart/docs/airflow-configuration.rst
@@ -28,7 +28,7 @@ allow webserver users to view the config from within the UI:
 .. code-block:: yaml
 
    config:
-     webserver:
+     api:
        expose_config: 'True'  # by default this is 'False'
 
 Generally speaking, it is useful to familiarize oneself with the Airflow
diff --git a/clients/python/README.md b/clients/python/README.md
index 07ae5c81b3a..0fa289542a5 100644
--- a/clients/python/README.md
+++ b/clients/python/README.md
@@ -555,11 +555,11 @@ You can also set it by env variable: `export AIRFLOW__CORE__LOAD_EXAMPLES=True`
   In the `[webserver]` section of your `airflow.cfg` set:
 
 ```ini
-[webserver]
+[api]
 expose_config = True
 ```
 
-You can also set it by env variable: `export AIRFLOW__WEBSERVER__EXPOSE_CONFIG=True`
+You can also set it by env variable: `export AIRFLOW__API__EXPOSE_CONFIG=True`
 
 * Configure your host/ip/user/password in the `test_python_client.py` file
 
diff --git a/dev/README_RELEASE_PYTHON_CLIENT.md b/dev/README_RELEASE_PYTHON_CLIENT.md
index dab9bf9e41e..be46acce909 100644
--- a/dev/README_RELEASE_PYTHON_CLIENT.md
+++ b/dev/README_RELEASE_PYTHON_CLIENT.md
@@ -478,7 +478,7 @@ and allows you to test the client in a real environment.
 
 ```shell
 export AIRFLOW__API__AUTH_BACKENDS=airflow.providers.fab.auth_manager.api.auth.backend.session,airflow.providers.fab.auth_manager.api.auth.backend.basic_auth
-export AIRFLOW__WEBSERVER__EXPOSE_CONFIG=True
+export AIRFLOW__API__EXPOSE_CONFIG=True
 ```
 
 
@@ -487,7 +487,7 @@ or `http://localhost:28080` from the host) and you should be able to access the
 with `admin`/`admin` credentials. The `http://localhost:8080` and `admin`/`admin` credentials are
 default in the `clients/python/test_python_client.py` test.
 
-The ``AIRFLOW__WEBSERVER__EXPOSE_CONFIG`` is optional - the script will also succeed when
+The ``AIRFLOW__API__EXPOSE_CONFIG`` is optional - the script will also succeed when
 (default setting) exposing configuration is disabled.
 
 2. Start Airflow in Breeze with example dags enabled:
diff --git a/scripts/docker/entrypoint_ci.sh b/scripts/docker/entrypoint_ci.sh
index 67ae117fda6..7813c351b31 100755
--- a/scripts/docker/entrypoint_ci.sh
+++ b/scripts/docker/entrypoint_ci.sh
@@ -355,7 +355,7 @@ function start_api_server_with_examples(){
         return
     fi
     export AIRFLOW__CORE__LOAD_EXAMPLES=True
-    export AIRFLOW__WEBSERVER__EXPOSE_CONFIG=True
+    export AIRFLOW__API__EXPOSE_CONFIG=True
     echo
     echo "${COLOR_BLUE}Initializing database${COLOR_RESET}"
     echo


(airflow) 06/25: Refactor Dashboard to enhance layout (#50026)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit b9d5264a654d12b0948c3d48f3334f201029405a
Author: Guan Ming(Wesley) Chiu <10...@users.noreply.github.com>
AuthorDate: Thu May 1 03:09:23 2025 +0800

    Refactor Dashboard to enhance layout (#50026)
    
    (cherry picked from commit d10f1ccd03de6e6cd1f771bb60aadf552ea8e8fb)
---
 .../Dashboard/HistoricalMetrics/DagRunMetrics.tsx  | 31 +++++++++--------
 .../Dashboard/HistoricalMetrics/MetricSection.tsx  |  2 +-
 .../HistoricalMetrics/TaskInstanceMetrics.tsx      | 39 ++++++++++++----------
 3 files changed, 39 insertions(+), 33 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx
index dc0bb6b5cf7..5ff72147093 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Heading, HStack } from "@chakra-ui/react";
+import { Box, Separator, Heading, HStack, Stack } from "@chakra-ui/react";
 import type { DAGRunStates } from "openapi-gen/requests/types.gen";
 import { FiBarChart } from "react-icons/fi";
 import { Link as RouterLink } from "react-router-dom";
@@ -35,8 +35,8 @@ type DagRunMetricsProps = {
 const DAGRUN_STATES: Array<keyof DAGRunStates> = ["queued", "running", "success", "failed"];
 
 export const DagRunMetrics = ({ dagRunStates, endDate, startDate, total }: DagRunMetricsProps) => (
-  <Box borderRadius={5} borderWidth={1} p={2}>
-    <HStack mb={4}>
+  <Box borderRadius={5} borderWidth={1} p={4}>
+    <HStack>
       <RouterLink
         to={`/dag_runs?start_date=${startDate}${endDate === undefined ? "" : `&end_date=${endDate}`}`}
       >
@@ -47,16 +47,19 @@ export const DagRunMetrics = ({ dagRunStates, endDate, startDate, total }: DagRu
       </RouterLink>
       <Heading size="md">Dag Runs</Heading>
     </HStack>
-    {DAGRUN_STATES.map((state) => (
-      <MetricSection
-        endDate={endDate}
-        key={state}
-        kind="dag_runs"
-        runs={dagRunStates[state]}
-        startDate={startDate}
-        state={state}
-        total={total}
-      />
-    ))}
+    <Separator my={2} />
+    <Stack gap={4}>
+      {DAGRUN_STATES.map((state) => (
+        <MetricSection
+          endDate={endDate}
+          key={state}
+          kind="dag_runs"
+          runs={dagRunStates[state]}
+          startDate={startDate}
+          state={state}
+          total={total}
+        />
+      ))}
+    </Stack>
   </Box>
 );
diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx
index cf50400fab9..01550ba4efc 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx
@@ -49,7 +49,7 @@ export const MetricSection = ({ endDate, kind, runs, startDate, state, total }:
   }
 
   return (
-    <VStack align="left" gap={1} mb={4} ml={0} pl={0}>
+    <VStack align="left" gap={1} ml={0} pl={0}>
       <Flex justify="space-between">
         <HStack>
           <RouterLink to={`/${kind}?${searchParams.toString()}`}>
diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/TaskInstanceMetrics.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/TaskInstanceMetrics.tsx
index a9975032e9b..081a8de935f 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/TaskInstanceMetrics.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/TaskInstanceMetrics.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Heading, HStack } from "@chakra-ui/react";
+import { Box, Separator, Heading, HStack, Stack } from "@chakra-ui/react";
 import type { TaskInstanceState, TaskInstanceStateCount } from "openapi-gen/requests/types.gen";
 import { MdOutlineTask } from "react-icons/md";
 import { Link as RouterLink } from "react-router-dom";
@@ -54,8 +54,8 @@ export const TaskInstanceMetrics = ({
   taskInstanceStates,
   total,
 }: TaskInstanceMetricsProps) => (
-  <Box borderRadius={5} borderWidth={1} mt={2} p={2}>
-    <HStack mb={4}>
+  <Box borderRadius={5} borderWidth={1} mt={2} p={4}>
+    <HStack>
       <RouterLink
         to={`/task_instances?start_date=${startDate}${endDate === undefined ? "" : `&end_date=${endDate}`}`}
       >
@@ -66,20 +66,23 @@ export const TaskInstanceMetrics = ({
       </RouterLink>
       <Heading size="md">Task Instances</Heading>
     </HStack>
-    {TASK_STATES.sort((stateA, stateB) =>
-      taskInstanceStates[stateA] > taskInstanceStates[stateB] ? -1 : 1,
-    ).map((state) =>
-      taskInstanceStates[state] > 0 ? (
-        <MetricSection
-          endDate={endDate}
-          key={state}
-          kind="task_instances"
-          runs={taskInstanceStates[state]}
-          startDate={startDate}
-          state={state as TaskInstanceState}
-          total={total}
-        />
-      ) : undefined,
-    )}
+    <Separator my={2} />
+    <Stack gap={4}>
+      {TASK_STATES.sort((stateA, stateB) =>
+        taskInstanceStates[stateA] > taskInstanceStates[stateB] ? -1 : 1,
+      ).map((state) =>
+        taskInstanceStates[state] > 0 ? (
+          <MetricSection
+            endDate={endDate}
+            key={state}
+            kind="task_instances"
+            runs={taskInstanceStates[state]}
+            startDate={startDate}
+            state={state as TaskInstanceState}
+            total={total}
+          />
+        ) : undefined,
+      )}
+    </Stack>
   </Box>
 );


(airflow) 13/25: Add auto-refresh for `Stats` (#50088)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit e3b068ddb30927f1f13b4c2c6152e95adfff3adb
Author: Guan Ming(Wesley) Chiu <10...@users.noreply.github.com>
AuthorDate: Fri May 2 04:40:36 2025 +0800

    Add auto-refresh for `Stats` (#50088)
    
    (cherry picked from commit 445e188ac666572e97210a12830c546fd75e150f)
---
 airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
index 03149103894..3e13ab64d62 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
@@ -20,12 +20,16 @@ import { Box, Flex, Heading, HStack } from "@chakra-ui/react";
 import { FiClipboard, FiZap } from "react-icons/fi";
 
 import { useDashboardServiceDagStats } from "openapi/queries";
+import { useAutoRefresh } from "src/utils";
 
 import { DAGImportErrors } from "./DAGImportErrors";
 import { StatsCard } from "./StatsCard";
 
 export const Stats = () => {
-  const { data: statsData, isLoading: isStatsLoading } = useDashboardServiceDagStats();
+  const refetchInterval = useAutoRefresh({});
+  const { data: statsData, isLoading: isStatsLoading } = useDashboardServiceDagStats(undefined, {
+    refetchInterval,
+  });
   const failedDagsCount = statsData?.failed_dag_count ?? 0;
   const queuedDagsCount = statsData?.queued_dag_count ?? 0;
   const runningDagsCount = statsData?.running_dag_count ?? 0;


(airflow) 18/25: Added focus view on grid and graph on second click (#50125)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 44bb1010372eaabb39a87a232d97531888b82f09
Author: Dominic Leung <le...@gmail.com>
AuthorDate: Sat May 3 05:31:37 2025 +0800

    Added focus view on grid and graph on second click (#50125)
    
    (cherry picked from commit e28d460d9fbfe8fc9679ed4bcce5b84872925ec2)
---
 .../ui/src/layouts/Details/DetailsLayout.tsx       | 12 ++++++--
 .../ui/src/layouts/Details/PanelButtons.tsx        | 35 ++++++++++++++++++++--
 2 files changed, 42 insertions(+), 5 deletions(-)

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 69d32034fc4..bae001db6f3 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
@@ -18,6 +18,7 @@
  */
 import { Box, HStack, Flex, useDisclosure } from "@chakra-ui/react";
 import { useReactFlow } from "@xyflow/react";
+import { useRef } from "react";
 import type { PropsWithChildren, ReactNode } from "react";
 import { LuFileWarning } from "react-icons/lu";
 import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
@@ -53,6 +54,7 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => {
 
   const { data: dag } = useDagServiceGetDag({ dagId });
   const [defaultDagView] = useLocalStorage<"graph" | "grid">("default_dag_view", "grid");
+  const panelGroupRef = useRef(null);
   const [dagView, setDagView] = useLocalStorage<"graph" | "grid">(`dag_view-${dagId}`, defaultDagView);
   const [limit, setLimit] = useLocalStorage<number>(`dag_runs_limit-${dagId}`, 10);
 
@@ -75,10 +77,16 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => {
       <Toaster />
       <BackfillBanner dagId={dagId} />
       <Box flex={1} minH={0}>
-        <PanelGroup autoSaveId={dagId} direction="horizontal">
+        <PanelGroup autoSaveId={dagId} direction="horizontal" ref={panelGroupRef}>
           <Panel defaultSize={dagView === "graph" ? 70 : 20} minSize={6}>
             <Box height="100%" overflowY="auto" position="relative" pr={2}>
-              <PanelButtons dagView={dagView} limit={limit} setDagView={setDagView} setLimit={setLimit} />
+              <PanelButtons
+                dagView={dagView}
+                limit={limit}
+                panelGroupRef={panelGroupRef}
+                setDagView={setDagView}
+                setLimit={setLimit}
+              />
               {dagView === "graph" ? <Graph /> : <Grid limit={limit} />}
             </Box>
           </Panel>
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 384eaa407c2..4d18f87d921 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
@@ -26,6 +26,7 @@ import {
   Portal,
   Select,
 } from "@chakra-ui/react";
+import { useReactFlow } from "@xyflow/react";
 import { FiChevronDown, FiGrid } from "react-icons/fi";
 import { MdOutlineAccountTree } from "react-icons/md";
 import { useParams } from "react-router-dom";
@@ -41,6 +42,7 @@ import { ToggleGroups } from "./ToggleGroups";
 type Props = {
   readonly dagView: string;
   readonly limit: number;
+  readonly panelGroupRef: React.RefObject<{ setLayout?: (layout: Array<number>) => void } & HTMLDivElement>;
   readonly setDagView: (x: "graph" | "grid") => void;
   readonly setLimit: React.Dispatch<React.SetStateAction<number>>;
 };
@@ -68,8 +70,9 @@ const deps = ["all", "immediate", "tasks"];
 
 type Dependency = (typeof deps)[number];
 
-export const PanelButtons = ({ dagView, limit, setDagView, setLimit }: Props) => {
+export const PanelButtons = ({ dagView, limit, panelGroupRef, setDagView, setLimit }: Props) => {
   const { dagId = "" } = useParams();
+  const { fitView } = useReactFlow();
   const [dependencies, setDependencies, removeDependencies] = useLocalStorage<Dependency>(
     `dependencies-${dagId}`,
     "tasks",
@@ -97,13 +100,34 @@ export const PanelButtons = ({ dagView, limit, setDagView, setLimit }: Props) =>
     }
   };
 
+  const handleFocus = (view: string) => {
+    if (panelGroupRef.current) {
+      const panelGroup = panelGroupRef.current;
+
+      if (typeof panelGroup.setLayout === "function") {
+        const newLayout = view === "graph" ? [70, 30] : [30, 70];
+
+        panelGroup.setLayout(newLayout);
+        // Used setTimeout to ensure DOM has been updated
+        setTimeout(() => {
+          void fitView();
+        }, 1);
+      }
+    }
+  };
+
   return (
     <Flex justifyContent="space-between" position="absolute" top={1} width="100%" zIndex={1}>
       <ButtonGroup attached size="sm" variant="outline">
         <IconButton
           aria-label="Show Grid"
           colorPalette="blue"
-          onClick={() => setDagView("grid")}
+          onClick={() => {
+            setDagView("grid");
+            if (dagView === "grid") {
+              handleFocus("grid");
+            }
+          }}
           title="Show Grid"
           variant={dagView === "grid" ? "solid" : "outline"}
         >
@@ -112,7 +136,12 @@ export const PanelButtons = ({ dagView, limit, setDagView, setLimit }: Props) =>
         <IconButton
           aria-label="Show Graph"
           colorPalette="blue"
-          onClick={() => setDagView("graph")}
+          onClick={() => {
+            setDagView("graph");
+            if (dagView === "graph") {
+              handleFocus("graph");
+            }
+          }}
           title="Show Graph"
           variant={dagView === "graph" ? "solid" : "outline"}
         >


(airflow) 01/25: docs(ruff): fix outdated content and add airflow rule list, ruff configuration links (#50232)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 43aaec812712368b597b482cf40bf5b4580974e9
Author: Wei Lee <we...@gmail.com>
AuthorDate: Tue May 6 20:39:26 2025 +0800

    docs(ruff): fix outdated content and add airflow rule list, ruff configuration links (#50232)
    
    (cherry picked from commit ea61ff7b6f8e7353704ca2f7aafd316ac5515336)
---
 airflow-core/docs/best-practices.rst                     | 10 +++-------
 airflow-core/docs/installation/upgrading_to_airflow3.rst |  3 +++
 2 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/airflow-core/docs/best-practices.rst b/airflow-core/docs/best-practices.rst
index bc4889b5f20..268f3e7150f 100644
--- a/airflow-core/docs/best-practices.rst
+++ b/airflow-core/docs/best-practices.rst
@@ -303,11 +303,7 @@ Code Quality and Linting
 
 Maintaining high code quality is essential for the reliability and maintainability of your Airflow workflows. Utilizing linting tools can help identify potential issues and enforce coding standards. One such tool is ``ruff``, a fast Python linter that now includes specific rules for Airflow.
 
-ruff assists in detecting deprecated features and patterns that may affect your migration to Airflow 3.0. For instance, it includes rules prefixed with ``AIR`` to flag potential issues:
-
-- **AIR301**: Flags DAGs without an explicit ``schedule`` argument.
-- **AIR302**: Identifies usage of deprecated ``schedule_interval`` parameter.
-- **AIR303**: Detects imports from modules that have been relocated or removed in Airflow 3.0.
+ruff assists in detecting deprecated features and patterns that may affect your migration to Airflow 3.0. For instance, it includes rules prefixed with ``AIR`` to flag potential issues. The full list is detailed in `Airflow (AIR) <https://docs.astral.sh/ruff/rules/#airflow-air>`_.
 
 Installing and Using ruff
 -------------------------
@@ -316,13 +312,13 @@ Installing and Using ruff
 
    .. code-block:: bash
 
-      pip install "ruff>=0.9.5"
+      pip install "ruff>=0.11.6"
 
 2. **Running ruff**: Execute ``ruff`` to check your dags for potential issues:
 
    .. code-block:: bash
 
-      ruff check dags/ --select AIR301,AIR302,AIR303
+      ruff check dags/ --select AIR3 --preview
 
    This command will analyze your dags located in the ``dags/`` directory and report any issues related to the specified rules.
 
diff --git a/airflow-core/docs/installation/upgrading_to_airflow3.rst b/airflow-core/docs/installation/upgrading_to_airflow3.rst
index 88057087967..9921f89416e 100644
--- a/airflow-core/docs/installation/upgrading_to_airflow3.rst
+++ b/airflow-core/docs/installation/upgrading_to_airflow3.rst
@@ -70,6 +70,9 @@ Some changes can be automatically fixed. To do so, run the following command:
 
     ruff check dag/ --select AIR301 --fix --preview
 
+
+You can also configure these flags through configuration files. See `Configuring Ruff <Configuring Ruff>`_ for details.
+
 Step 4: Install the Standard Providers
 --------------------------------------
 


(airflow) 12/25: Fix date range field alignment (#50086)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 6c42950f543ef2e0e212552ef92781a55fa86e4b
Author: Guan Ming(Wesley) Chiu <10...@users.noreply.github.com>
AuthorDate: Fri May 2 04:12:41 2025 +0800

    Fix date range field alignment (#50086)
    
    (cherry picked from commit 182c7f1194e6b62ae131314c5b445985de5c1dfd)
---
 .../src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx         | 1 +
 1 file changed, 1 insertion(+)

diff --git a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
index b86aa824c9b..668f54a3d4c 100644
--- a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
@@ -175,6 +175,7 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
                 <Field.Root invalid={Boolean(errors.date) || dataIntervalInvalid} required>
                   <Field.Label>To</Field.Label>
                   <DateTimeInput {...field} max={today} onBlur={resetDateError} size="sm" />
+                  <Field.ErrorText>&nbsp;</Field.ErrorText>
                 </Field.Root>
               )}
             />


(airflow) 03/25: Add `dag_run_conf` to `RunBackfillForm` (#49763)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 93960d8d194ded926b212c90dcc9bdd7c5c9bded
Author: Guan Ming(Wesley) Chiu <10...@users.noreply.github.com>
AuthorDate: Thu May 1 01:49:30 2025 +0800

    Add `dag_run_conf` to `RunBackfillForm` (#49763)
    
    * feat: add `dag_run_conf` to `RunBackfillForm`
    
    Co-authored-by: Brent Bovenzi <46...@users.noreply.github.com>
    
    * fix: add missing conf preset
    
    Co-authored-by: Brent Bovenzi <46...@users.noreply.github.com>
    
    ---------
    
    Co-authored-by: Brent Bovenzi <46...@users.noreply.github.com>
    (cherry picked from commit 6ca99d898e0a65a41b47e0188255ad1cb4068ca9)
---
 .../src/airflow/ui/src/components/ConfigForm.tsx   | 118 ++++++++++++++
 .../src/components/DagActions/RunBackfillForm.tsx  |  37 ++++-
 .../src/components/TriggerDag/TriggerDAGForm.tsx   | 174 +++++++--------------
 3 files changed, 206 insertions(+), 123 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx
new file mode 100644
index 00000000000..a216b22de60
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx
@@ -0,0 +1,118 @@
+/*!
+ * 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 { Accordion, Box, Field } from "@chakra-ui/react";
+import { type Control, type FieldValues, type Path, Controller } from "react-hook-form";
+
+import type { ParamsSpec } from "src/queries/useDagParams";
+import { useParamStore } from "src/queries/useParamStore";
+
+import { FlexibleForm, flexibleFormDefaultSection } from "./FlexibleForm";
+import { JsonEditor } from "./JsonEditor";
+
+type ConfigFormProps<T extends FieldValues = FieldValues> = {
+  readonly children?: React.ReactNode;
+  readonly control: Control<T>;
+  readonly errors: {
+    conf?: string;
+    date?: unknown;
+  };
+  readonly initialParamsDict: { paramsDict: ParamsSpec };
+  readonly setErrors: React.Dispatch<
+    React.SetStateAction<{
+      conf?: string;
+      date?: unknown;
+    }>
+  >;
+};
+
+const ConfigForm = <T extends FieldValues = FieldValues>({
+  children,
+  control,
+  errors,
+  initialParamsDict,
+  setErrors,
+}: ConfigFormProps<T>) => {
+  const { conf, setConf } = useParamStore();
+
+  const validateAndPrettifyJson = (value: string) => {
+    try {
+      const parsedJson = JSON.parse(value) as JSON;
+
+      setErrors((prev) => ({ ...prev, conf: undefined }));
+
+      const formattedJson = JSON.stringify(parsedJson, undefined, 2);
+
+      if (formattedJson !== conf) {
+        setConf(formattedJson); // Update only if the value is different
+      }
+
+      return formattedJson;
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : "Unknown error occurred.";
+
+      setErrors((prev) => ({
+        ...prev,
+        conf: `Invalid JSON format: ${errorMessage}`,
+      }));
+
+      return value;
+    }
+  };
+
+  return (
+    <Accordion.Root
+      collapsible
+      defaultValue={[flexibleFormDefaultSection]}
+      mb={4}
+      size="lg"
+      variant="enclosed"
+    >
+      <FlexibleForm
+        flexibleFormDefaultSection={flexibleFormDefaultSection}
+        initialParamsDict={initialParamsDict}
+      />
+      <Accordion.Item key="advancedOptions" value="advancedOptions">
+        <Accordion.ItemTrigger cursor="button">Advanced Options</Accordion.ItemTrigger>
+        <Accordion.ItemContent>
+          <Box p={4}>
+            {children}
+            <Controller
+              control={control}
+              name={"conf" as Path<T>}
+              render={({ field }) => (
+                <Field.Root invalid={Boolean(errors.conf)} mt={6}>
+                  <Field.Label fontSize="md">Configuration JSON</Field.Label>
+                  <JsonEditor
+                    {...field}
+                    onBlur={() => {
+                      field.onChange(validateAndPrettifyJson(field.value as string));
+                    }}
+                  />
+                  {Boolean(errors.conf) ? <Field.ErrorText>{errors.conf}</Field.ErrorText> : undefined}
+                </Field.Root>
+              )}
+            />
+          </Box>
+        </Accordion.ItemContent>
+      </Accordion.Item>
+    </Accordion.Root>
+  );
+};
+
+export default ConfigForm;
diff --git a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
index 4ab35b2f466..2079de6c604 100644
--- a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
@@ -25,11 +25,15 @@ import { Button } from "src/components/ui";
 import { reprocessBehaviors } from "src/constants/reprocessBehaviourParams";
 import { useCreateBackfill } from "src/queries/useCreateBackfill";
 import { useCreateBackfillDryRun } from "src/queries/useCreateBackfillDryRun";
+import { useDagParams } from "src/queries/useDagParams";
+import { useParamStore } from "src/queries/useParamStore";
 import { useTogglePause } from "src/queries/useTogglePause";
 import { pluralize } from "src/utils";
 
+import ConfigForm from "../ConfigForm";
 import { DateTimeInput } from "../DateTimeInput";
 import { ErrorAlert } from "../ErrorAlert";
+import type { DagRunTriggerParams } from "../TriggerDag/TriggerDAGForm";
 import { Checkbox } from "../ui/Checkbox";
 import { RadioCardItem, RadioCardLabel, RadioCardRoot } from "../ui/RadioCard";
 
@@ -39,14 +43,17 @@ type RunBackfillFormProps = {
 };
 const today = new Date().toISOString().slice(0, 16);
 
+type BackfillFormProps = DagRunTriggerParams & Omit<BackfillPostBody, "dag_run_conf">;
+
 const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
   const [errors, setErrors] = useState<{ conf?: string; date?: unknown }>({});
   const [unpause, setUnpause] = useState(true);
-
-  const { control, handleSubmit, reset, watch } = useForm<BackfillPostBody>({
+  const initialParamsDict = useDagParams(dag.dag_id, true);
+  const { conf } = useParamStore();
+  const { control, handleSubmit, reset, watch } = useForm<BackfillFormProps>({
     defaultValues: {
+      conf,
       dag_id: dag.dag_id,
-      dag_run_conf: {},
       from_date: "",
       max_active_runs: 1,
       reprocess_behavior: "none",
@@ -55,7 +62,7 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
     },
     mode: "onBlur",
   });
-  const values = useWatch<BackfillPostBody>({
+  const values = useWatch<BackfillFormProps>({
     control,
   });
 
@@ -85,10 +92,16 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
     }
   }, [dateValidationError]);
 
+  useEffect(() => {
+    if (conf) {
+      reset({ conf });
+    }
+  }, [conf, reset]);
+
   const dataIntervalStart = watch("from_date");
   const dataIntervalEnd = watch("to_date");
 
-  const onSubmit = (fdata: BackfillPostBody) => {
+  const onSubmit = (fdata: BackfillFormProps) => {
     if (unpause && dag.is_paused) {
       togglePause({
         dagId: dag.dag_id,
@@ -98,11 +111,14 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
       });
     }
     createBackfill({
-      requestBody: fdata,
+      requestBody: {
+        ...fdata,
+        dag_run_conf: JSON.parse(fdata.conf) as Record<string, unknown>,
+      },
     });
   };
 
-  const onCancel = (fdata: BackfillPostBody) => {
+  const onCancel = (fdata: BackfillFormProps) => {
     reset(fdata);
     onClose();
   };
@@ -237,6 +253,13 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
             <Spacer />
           </>
         ) : undefined}
+
+        <ConfigForm
+          control={control}
+          errors={errors}
+          initialParamsDict={initialParamsDict}
+          setErrors={setErrors}
+        />
       </VStack>
       <Box as="footer" display="flex" justifyContent="flex-end" mt={4}>
         <HStack w="full">
diff --git a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
index b3d470ba417..fdeae4bdb69 100644
--- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
@@ -16,10 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Input, Button, Box, Spacer, HStack, Field, Stack } from "@chakra-ui/react";
+import { Button, Box, Spacer, HStack, Input, Field, Stack } from "@chakra-ui/react";
 import dayjs from "dayjs";
 import { useEffect, useState } from "react";
-import { useForm, Controller } from "react-hook-form";
+import { Controller, useForm } from "react-hook-form";
 import { FiPlay } from "react-icons/fi";
 
 import { useDagParams } from "src/queries/useDagParams";
@@ -27,11 +27,9 @@ import { useParamStore } from "src/queries/useParamStore";
 import { useTogglePause } from "src/queries/useTogglePause";
 import { useTrigger } from "src/queries/useTrigger";
 
+import ConfigForm from "../ConfigForm";
 import { DateTimeInput } from "../DateTimeInput";
 import { ErrorAlert } from "../ErrorAlert";
-import { FlexibleForm, flexibleFormDefaultSection } from "../FlexibleForm";
-import { JsonEditor } from "../JsonEditor";
-import { Accordion } from "../ui";
 import { Checkbox } from "../ui/Checkbox";
 import EditableMarkdown from "./EditableMarkdown";
 
@@ -53,7 +51,7 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: TriggerDAGFormProps)
   const [errors, setErrors] = useState<{ conf?: string; date?: unknown }>({});
   const initialParamsDict = useDagParams(dagId, open);
   const { error: errorTrigger, isPending, triggerDagRun } = useTrigger({ dagId, onSuccessConfirm: onClose });
-  const { conf, setConf } = useParamStore();
+  const { conf } = useParamStore();
   const [unpause, setUnpause] = useState(true);
 
   const { mutate: togglePause } = useTogglePause({ dagId });
@@ -75,6 +73,10 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: TriggerDAGFormProps)
     }
   }, [conf, reset]);
 
+  const resetDateError = () => {
+    setErrors((prev) => ({ ...prev, date: undefined }));
+  };
+
   const onSubmit = (data: DagRunTriggerParams) => {
     if (unpause && isPaused) {
       togglePause({
@@ -87,119 +89,59 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: TriggerDAGFormProps)
     triggerDagRun(data);
   };
 
-  const validateAndPrettifyJson = (value: string) => {
-    try {
-      const parsedJson = JSON.parse(value) as JSON;
-
-      setErrors((prev) => ({ ...prev, conf: undefined }));
-
-      const formattedJson = JSON.stringify(parsedJson, undefined, 2);
-
-      if (formattedJson !== conf) {
-        setConf(formattedJson); // Update only if the value is different
-      }
-
-      return formattedJson;
-    } catch (error) {
-      const errorMessage = error instanceof Error ? error.message : "Unknown error occurred.";
-
-      setErrors((prev) => ({
-        ...prev,
-        conf: `Invalid JSON format: ${errorMessage}`,
-      }));
-
-      return value;
-    }
-  };
-
-  const resetDateError = () => {
-    setErrors((prev) => ({ ...prev, date: undefined }));
-  };
-
   return (
-    <>
-      <Accordion.Root
-        collapsible
-        defaultValue={[flexibleFormDefaultSection]}
-        mb={4}
-        mt={8}
-        size="lg"
-        variant="enclosed"
+    <Box mt={8}>
+      <ConfigForm
+        control={control}
+        errors={errors}
+        initialParamsDict={initialParamsDict}
+        setErrors={setErrors}
       >
-        <FlexibleForm
-          flexibleFormDefaultSection={flexibleFormDefaultSection}
-          initialParamsDict={initialParamsDict}
+        <Controller
+          control={control}
+          name="logicalDate"
+          render={({ field }) => (
+            <Field.Root invalid={Boolean(errors.date)} orientation="horizontal">
+              <Stack>
+                <Field.Label fontSize="md" style={{ flexBasis: "30%" }}>
+                  Logical Date
+                </Field.Label>
+              </Stack>
+              <Stack css={{ flexBasis: "70%" }}>
+                <DateTimeInput {...field} onBlur={resetDateError} size="sm" />
+              </Stack>
+            </Field.Root>
+          )}
         />
-        <Accordion.Item key="advancedOptions" value="advancedOptions">
-          <Accordion.ItemTrigger cursor="button">Advanced Options</Accordion.ItemTrigger>
-          <Accordion.ItemContent>
-            <Box p={4}>
-              <Controller
-                control={control}
-                name="logicalDate"
-                render={({ field }) => (
-                  <Field.Root invalid={Boolean(errors.date)} orientation="horizontal">
-                    <Stack>
-                      <Field.Label fontSize="md" style={{ flexBasis: "30%" }}>
-                        Logical Date
-                      </Field.Label>
-                    </Stack>
-                    <Stack css={{ flexBasis: "70%" }}>
-                      <DateTimeInput {...field} onBlur={resetDateError} size="sm" />
-                    </Stack>
-                  </Field.Root>
-                )}
-              />
-
-              <Controller
-                control={control}
-                name="dagRunId"
-                render={({ field }) => (
-                  <Field.Root mt={6} orientation="horizontal">
-                    <Stack>
-                      <Field.Label fontSize="md" style={{ flexBasis: "30%" }}>
-                        Run ID
-                      </Field.Label>
-                    </Stack>
-                    <Stack css={{ flexBasis: "70%" }}>
-                      <Input {...field} size="sm" />
-                      <Field.HelperText>Optional - will be generated if not provided</Field.HelperText>
-                    </Stack>
-                  </Field.Root>
-                )}
-              />
-
-              <Controller
-                control={control}
-                name="conf"
-                render={({ field }) => (
-                  <Field.Root invalid={Boolean(errors.conf)} mt={6}>
-                    <Field.Label fontSize="md">Configuration JSON</Field.Label>
-                    <JsonEditor
-                      {...field}
-                      onBlur={() => {
-                        field.onChange(validateAndPrettifyJson(field.value));
-                      }}
-                    />
-                    {Boolean(errors.conf) ? <Field.ErrorText>{errors.conf}</Field.ErrorText> : undefined}
-                  </Field.Root>
-                )}
-              />
 
-              <Controller
-                control={control}
-                name="note"
-                render={({ field }) => (
-                  <Field.Root mt={6}>
-                    <Field.Label fontSize="md">Dag Run Notes</Field.Label>
-                    <EditableMarkdown field={field} placeholder="Click to add note" />
-                  </Field.Root>
-                )}
-              />
-            </Box>
-          </Accordion.ItemContent>
-        </Accordion.Item>
-      </Accordion.Root>
+        <Controller
+          control={control}
+          name="dagRunId"
+          render={({ field }) => (
+            <Field.Root mt={6} orientation="horizontal">
+              <Stack>
+                <Field.Label fontSize="md" style={{ flexBasis: "30%" }}>
+                  Run ID
+                </Field.Label>
+              </Stack>
+              <Stack css={{ flexBasis: "70%" }}>
+                <Input {...field} size="sm" />
+                <Field.HelperText>Optional - will be generated if not provided</Field.HelperText>
+              </Stack>
+            </Field.Root>
+          )}
+        />
+        <Controller
+          control={control}
+          name="note"
+          render={({ field }) => (
+            <Field.Root mt={6}>
+              <Field.Label fontSize="md">Dag Run Notes</Field.Label>
+              <EditableMarkdown field={field} placeholder="Click to add note" />
+            </Field.Root>
+          )}
+        />
+      </ConfigForm>
       {isPaused ? (
         <Checkbox checked={unpause} colorPalette="blue" onChange={() => setUnpause(!unpause)}>
           Unpause {dagId} on trigger
@@ -218,7 +160,7 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: TriggerDAGFormProps)
           </Button>
         </HStack>
       </Box>
-    </>
+    </Box>
   );
 };
 


(airflow) 24/25: Handle MappedTaskGroup map indexes (#49996)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit f001dc2d9b20ee6714164c04ce873cc65ab56848
Author: Wei Lee <we...@gmail.com>
AuthorDate: Mon May 5 22:42:19 2025 +0800

    Handle MappedTaskGroup map indexes (#49996)
    
    * fix(task_group): correctly set upstream_map_indexes for mapped task group
    
    * refactor(task_instance): refactor how upstream_map_indexes is handled
    
    * test: fix broken tests
    
    * fix(task_group): fix maaping handling
    
    * refactor(task_instnaces): rewrite _get_upstream_map_indexes
    
    (cherry picked from commit 5adeac2c83e4c063e4599efa23575d2be60b2d13)
---
 .../execution_api/datamodels/taskinstance.py       |  2 +-
 .../execution_api/routes/task_instances.py         | 40 ++++++++++++++++++++--
 .../versions/head/test_task_instances.py           |  3 ++
 devel-common/src/tests_common/pytest_plugin.py     |  3 +-
 .../src/airflow/sdk/api/datamodels/_generated.py   |  4 ++-
 task-sdk/src/airflow/sdk/definitions/xcom_arg.py   | 23 +++++--------
 .../task_sdk/definitions/test_mappedoperator.py    | 15 +++++++-
 7 files changed, 70 insertions(+), 20 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py
index c3a8be3134c..b83d731a54e 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py
@@ -314,7 +314,7 @@ class TIRunContext(BaseModel):
     connections: Annotated[list[ConnectionResponse], Field(default_factory=list)]
     """Connections that can be accessed by the task instance."""
 
-    upstream_map_indexes: dict[str, int] | None = None
+    upstream_map_indexes: dict[str, int | list[int] | None] | None = None
 
     next_method: str | None = None
     """Method to call. Set when task resumes from a trigger."""
diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py
index f97a684e3ff..00a5ea9a5e6 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py
@@ -19,7 +19,8 @@ from __future__ import annotations
 
 import json
 from collections import defaultdict
-from typing import Annotated, Any
+from collections.abc import Iterator
+from typing import TYPE_CHECKING, Annotated, Any
 from uuid import UUID
 
 import structlog
@@ -55,9 +56,14 @@ from airflow.models.taskinstance import TaskInstance as TI, _stop_remaining_task
 from airflow.models.taskreschedule import TaskReschedule
 from airflow.models.trigger import Trigger
 from airflow.models.xcom import XComModel
+from airflow.sdk.definitions.taskgroup import MappedTaskGroup
 from airflow.utils import timezone
 from airflow.utils.state import DagRunState, TaskInstanceState, TerminalTIState
 
+if TYPE_CHECKING:
+    from airflow.sdk.types import Operator
+
+
 router = VersionedAPIRouter()
 
 ti_id_router = VersionedAPIRouter(
@@ -82,7 +88,10 @@ log = structlog.get_logger(__name__)
     response_model_exclude_unset=True,
 )
 def ti_run(
-    task_instance_id: UUID, ti_run_payload: Annotated[TIEnterRunningPayload, Body()], session: SessionDep
+    task_instance_id: UUID,
+    ti_run_payload: Annotated[TIEnterRunningPayload, Body()],
+    session: SessionDep,
+    request: Request,
 ) -> TIRunContext:
     """
     Run a TaskInstance.
@@ -233,6 +242,11 @@ def ti_run(
             or 0
         )
 
+        if dag := request.app.state.dag_bag.get_dag(ti.dag_id):
+            upstream_map_indexes = dict(_get_upstream_map_indexes(dag.get_task(ti.task_id), ti.map_index))
+        else:
+            upstream_map_indexes = None
+
         context = TIRunContext(
             dag_run=dr,
             task_reschedule_count=task_reschedule_count,
@@ -242,6 +256,7 @@ def ti_run(
             connections=[],
             xcom_keys_to_clear=xcom_keys,
             should_retry=_is_eligible_to_retry(previous_state, ti.try_number, ti.max_tries),
+            upstream_map_indexes=upstream_map_indexes,
         )
 
         # Only set if they are non-null
@@ -257,6 +272,27 @@ def ti_run(
         )
 
 
+def _get_upstream_map_indexes(
+    task: Operator, ti_map_index: int
+) -> Iterator[tuple[str, int | list[int] | None]]:
+    for upstream_task in task.upstream_list:
+        map_indexes: int | list[int] | None
+        if not isinstance(upstream_task.task_group, MappedTaskGroup):
+            # regular tasks or non-mapped task groups
+            map_indexes = None
+        elif task.task_group == upstream_task.task_group:
+            # tasks in the same mapped task group
+            # the task should use the map_index as the previous task in the same mapped task group
+            map_indexes = ti_map_index
+        else:
+            # tasks not in the same mapped task group
+            # the upstream mapped task group should combine the xcom as a list and return it
+            mapped_ti_count: int = upstream_task.task_group.get_parse_time_mapped_ti_count()
+            map_indexes = list(range(mapped_ti_count)) if mapped_ti_count is not None else None
+
+        yield upstream_task.task_id, map_indexes
+
+
 @ti_id_router.patch(
     "/{task_instance_id}/state",
     status_code=status.HTTP_204_NO_CONTENT,
diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py
index 41bba2c2bd5..95631484087 100644
--- a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py
+++ b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py
@@ -179,6 +179,7 @@ class TestTIRunState:
                 "consumed_asset_events": [],
             },
             "task_reschedule_count": 0,
+            "upstream_map_indexes": None,
             "max_tries": max_tries,
             "should_retry": should_retry,
             "variables": [],
@@ -257,6 +258,7 @@ class TestTIRunState:
         assert response.json() == {
             "dag_run": mock.ANY,
             "task_reschedule_count": 0,
+            "upstream_map_indexes": None,
             "max_tries": 0,
             "should_retry": False,
             "variables": [],
@@ -318,6 +320,7 @@ class TestTIRunState:
         assert response.json() == {
             "dag_run": mock.ANY,
             "task_reschedule_count": 0,
+            "upstream_map_indexes": None,
             "max_tries": 0,
             "should_retry": False,
             "variables": [],
diff --git a/devel-common/src/tests_common/pytest_plugin.py b/devel-common/src/tests_common/pytest_plugin.py
index b461ddc409d..2b31152719d 100644
--- a/devel-common/src/tests_common/pytest_plugin.py
+++ b/devel-common/src/tests_common/pytest_plugin.py
@@ -2088,7 +2088,7 @@ def create_runtime_ti(mocked_parse):
         run_type: str = "manual",
         try_number: int = 1,
         map_index: int | None = -1,
-        upstream_map_indexes: dict[str, int] | None = None,
+        upstream_map_indexes: dict[str, int | list[int] | None] | None = None,
         task_reschedule_count: int = 0,
         ti_id: UUID | None = None,
         conf: dict[str, Any] | None = None,
@@ -2143,6 +2143,7 @@ def create_runtime_ti(mocked_parse):
             task_reschedule_count=task_reschedule_count,
             max_tries=task_retries if max_tries is None else max_tries,
             should_retry=should_retry if should_retry is not None else try_number <= task_retries,
+            upstream_map_indexes=upstream_map_indexes,
         )
 
         if upstream_map_indexes is not None:
diff --git a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py
index a477c3ffc41..dbf39fa4ae1 100644
--- a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py
+++ b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py
@@ -480,7 +480,9 @@ class TIRunContext(BaseModel):
     max_tries: Annotated[int, Field(title="Max Tries")]
     variables: Annotated[list[VariableResponse] | None, Field(title="Variables")] = None
     connections: Annotated[list[ConnectionResponse] | None, Field(title="Connections")] = None
-    upstream_map_indexes: Annotated[dict[str, int] | None, Field(title="Upstream Map Indexes")] = None
+    upstream_map_indexes: Annotated[
+        dict[str, int | list[int] | None] | None, Field(title="Upstream Map Indexes")
+    ] = None
     next_method: Annotated[str | None, Field(title="Next Method")] = None
     next_kwargs: Annotated[dict[str, Any] | str | None, Field(title="Next Kwargs")] = None
     xcom_keys_to_clear: Annotated[list[str] | None, Field(title="Xcom Keys To Clear")] = None
diff --git a/task-sdk/src/airflow/sdk/definitions/xcom_arg.py b/task-sdk/src/airflow/sdk/definitions/xcom_arg.py
index 1adcb7efaa7..2a93585304c 100644
--- a/task-sdk/src/airflow/sdk/definitions/xcom_arg.py
+++ b/task-sdk/src/airflow/sdk/definitions/xcom_arg.py
@@ -339,22 +339,17 @@ class PlainXComArg(XComArg):
         if self.operator.is_mapped:
             return LazyXComSequence(xcom_arg=self, ti=ti)
         tg = self.operator.get_closest_mapped_task_group()
-        result = None
         if tg is None:
-            # regular task
-            result = ti.xcom_pull(
-                task_ids=task_id,
-                key=self.key,
-                default=NOTSET,
-                map_indexes=None,
-            )
+            map_indexes = None
         else:
-            # task from a task group
-            result = ti.xcom_pull(
-                task_ids=task_id,
-                key=self.key,
-                default=NOTSET,
-            )
+            upstream_map_indexes = getattr(ti, "_upstream_map_indexes", {})
+            map_indexes = upstream_map_indexes.get(task_id, None)
+        result = ti.xcom_pull(
+            task_ids=task_id,
+            key=self.key,
+            default=NOTSET,
+            map_indexes=map_indexes,
+        )
         if not isinstance(result, ArgNotSet):
             return result
         if self.key == XCOM_RETURN_KEY:
diff --git a/task-sdk/tests/task_sdk/definitions/test_mappedoperator.py b/task-sdk/tests/task_sdk/definitions/test_mappedoperator.py
index d6b8ca8da4e..6cdb4b520f2 100644
--- a/task-sdk/tests/task_sdk/definitions/test_mappedoperator.py
+++ b/task-sdk/tests/task_sdk/definitions/test_mappedoperator.py
@@ -627,9 +627,22 @@ def test_operator_mapped_task_group_receives_value(create_runtime_ti, mock_super
         "tg.t2": range(3),
         "t3": [None],
     }
+    upstream_map_indexes_per_task_id = {
+        ("tg.t1", 0): {},
+        ("tg.t1", 1): {},
+        ("tg.t1", 2): {},
+        ("tg.t2", 0): {"tg.t1": 0},
+        ("tg.t2", 1): {"tg.t1": 1},
+        ("tg.t2", 2): {"tg.t1": 2},
+        ("t3", None): {"tg.t2": [0, 1, 2]},
+    }
     for task in dag.tasks:
         for map_index in expansion_per_task_id[task.task_id]:
-            mapped_ti = create_runtime_ti(task=task.prepare_for_execution(), map_index=map_index)
+            mapped_ti = create_runtime_ti(
+                task=task.prepare_for_execution(),
+                map_index=map_index,
+                upstream_map_indexes=upstream_map_indexes_per_task_id[(task.task_id, map_index)],
+            )
             context = mapped_ti.get_template_context()
             mapped_ti.task.render_template_fields(context)
             mapped_ti.task.execute(context)


(airflow) 22/25: Add audit log extra to table and improve UX (#50100)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit a40c476b4ab23fbb7841a7aab0e682b61e19bab3
Author: Brent Bovenzi <br...@astronomer.io>
AuthorDate: Sun May 4 07:43:54 2025 -0400

    Add audit log extra to table and improve UX (#50100)
    
    * Add Audit Log events extra
    
    * Rearrange audit log columns for readability
    
    * Add expand/collapse all button for extra json
    
    (cherry picked from commit b7b0daf0850f42d2ac79f1b6b3ebbea97b4ab53e)
---
 .../src/airflow/ui/src/pages/Events/Events.tsx     | 105 +++++++++++++++------
 1 file changed, 75 insertions(+), 30 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx b/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx
index f93d8a07526..e1527e2c33a 100644
--- a/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx
@@ -16,8 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box } from "@chakra-ui/react";
+import { Box, ButtonGroup, Code, Flex, Heading, IconButton, useDisclosure } from "@chakra-ui/react";
 import type { ColumnDef } from "@tanstack/react-table";
+import { MdCompress, MdExpand } from "react-icons/md";
 import { useParams } from "react-router-dom";
 
 import { useEventLogServiceGetEventLogs } from "openapi/queries";
@@ -25,13 +26,17 @@ import type { EventLogResponse } from "openapi/requests/types.gen";
 import { DataTable } from "src/components/DataTable";
 import { useTableURLState } from "src/components/DataTable/useTableUrlState";
 import { ErrorAlert } from "src/components/ErrorAlert";
+import RenderedJsonField from "src/components/RenderedJsonField";
 import Time from "src/components/Time";
 
-const eventsColumn = (
-  dagId?: string,
-  runId?: string,
-  taskId?: string,
-): Array<ColumnDef<EventLogResponse>> => [
+type EventsColumn = {
+  dagId?: string;
+  open?: boolean;
+  runId?: string;
+  taskId?: string;
+};
+
+const eventsColumn = ({ dagId, open, runId, taskId }: EventsColumn): Array<ColumnDef<EventLogResponse>> => [
   {
     accessorKey: "when",
     cell: ({ row: { original } }) => <Time datetime={original.when} />,
@@ -41,6 +46,43 @@ const eventsColumn = (
       skeletonWidth: 10,
     },
   },
+  {
+    accessorKey: "event",
+    enableSorting: true,
+    header: "Event",
+    meta: {
+      skeletonWidth: 10,
+    },
+  },
+  {
+    accessorKey: "owner",
+    enableSorting: true,
+    header: "User",
+    meta: {
+      skeletonWidth: 10,
+    },
+  },
+  {
+    accessorKey: "extra",
+    cell: ({ row: { original } }) => {
+      if (original.extra !== null) {
+        try {
+          const parsed = JSON.parse(original.extra) as Record<string, unknown>;
+
+          return <RenderedJsonField content={parsed} jsonProps={{ collapsed: !open }} />;
+        } catch {
+          return <Code>{original.extra}</Code>;
+        }
+      }
+
+      return undefined;
+    },
+    enableSorting: false,
+    header: "Extra",
+    meta: {
+      skeletonWidth: 200,
+    },
+  },
   ...(Boolean(dagId)
     ? []
     : [
@@ -93,22 +135,6 @@ const eventsColumn = (
       skeletonWidth: 10,
     },
   },
-  {
-    accessorKey: "event",
-    enableSorting: true,
-    header: "Event",
-    meta: {
-      skeletonWidth: 10,
-    },
-  },
-  {
-    accessorKey: "owner",
-    enableSorting: true,
-    header: "User",
-    meta: {
-      skeletonWidth: 10,
-    },
-  },
 ];
 
 export const Events = () => {
@@ -116,15 +142,11 @@ export const Events = () => {
   const { setTableURLState, tableURLState } = useTableURLState();
   const { pagination, sorting } = tableURLState;
   const [sort] = sorting;
+  const { onClose, onOpen, open } = useDisclosure();
 
   const orderBy = sort ? `${sort.desc ? "-" : ""}${sort.id}` : "-when";
 
-  const {
-    data,
-    error: EventsError,
-    isFetching,
-    isLoading,
-  } = useEventLogServiceGetEventLogs(
+  const { data, error, isFetching, isLoading } = useEventLogServiceGetEventLogs(
     {
       dagId,
       limit: pagination.pageSize,
@@ -139,9 +161,32 @@ export const Events = () => {
 
   return (
     <Box>
-      <ErrorAlert error={EventsError} />
+      <Flex alignItems="center" justifyContent="space-between">
+        <Heading>Audit Log Events</Heading>
+        <ButtonGroup attached size="sm" variant="surface">
+          <IconButton
+            aria-label="Expand all extra json"
+            onClick={onOpen}
+            size="sm"
+            title="Expand all extra json"
+            variant="surface"
+          >
+            <MdExpand />
+          </IconButton>
+          <IconButton
+            aria-label="Collapse all extra json"
+            onClick={onClose}
+            size="sm"
+            title="Collapse all extra json"
+            variant="surface"
+          >
+            <MdCompress />
+          </IconButton>
+        </ButtonGroup>
+      </Flex>
+      <ErrorAlert error={error} />
       <DataTable
-        columns={eventsColumn(dagId, runId, taskId)}
+        columns={eventsColumn({ dagId, open, runId, taskId })}
         data={data ? data.event_logs : []}
         displayMode="table"
         initialState={tableURLState}


(airflow) 17/25: Add TIs tab to mapped task details (#50085)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit cae60348d192d205f6a3d18d4c41b0195996a2c6
Author: Brent Bovenzi <br...@astronomer.io>
AuthorDate: Fri May 2 17:25:23 2025 -0400

    Add TIs tab to mapped task details (#50085)
    
    * Add TIs tab to mapped task details
    
    * Adjust Tab title to total mapped count
    
    (cherry picked from commit b11895dcd39d0f33a91c4294d31665e0a862b60a)
---
 .../MappedTaskInstance/MappedTaskInstance.tsx      |  6 ++-
 .../ui/src/pages/TaskInstance/TaskInstance.tsx     | 43 ++++++++++++++++++++--
 2 files changed, 44 insertions(+), 5 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/MappedTaskInstance.tsx b/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/MappedTaskInstance.tsx
index 770c6e9348d..9225610bc24 100644
--- a/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/MappedTaskInstance.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/MappedTaskInstance.tsx
@@ -30,8 +30,6 @@ import { isStatePending, useAutoRefresh } from "src/utils";
 
 import { Header } from "./Header";
 
-const tabs = [{ icon: <MdOutlineTask />, label: "Task Instances", value: "" }];
-
 export const MappedTaskInstance = () => {
   const { dagId = "", runId = "", taskId = "" } = useParams();
   const refetchInterval = useAutoRefresh({ dagId });
@@ -74,6 +72,10 @@ export const MappedTaskInstance = () => {
     .find((dr) => dr.dag_run_id === runId)
     ?.task_instances.find((ti) => ti.task_id === taskId);
 
+  const tabs = [
+    { icon: <MdOutlineTask />, label: `Task Instances [${taskInstance?.task_count}]`, value: "" },
+  ];
+
   return (
     <ReactFlowProvider>
       <DetailsLayout dag={dag} error={error ?? dagError} isLoading={isLoading || isDagLoading} tabs={tabs}>
diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx
index f6993b87b27..8e19e7dbec2 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx
@@ -18,11 +18,15 @@
  */
 import { ReactFlowProvider } from "@xyflow/react";
 import { FiCode } from "react-icons/fi";
-import { MdDetails, MdOutlineEventNote, MdReorder, MdSyncAlt } from "react-icons/md";
+import { MdDetails, MdOutlineEventNote, MdOutlineTask, MdReorder, MdSyncAlt } from "react-icons/md";
 import { PiBracketsCurlyBold } from "react-icons/pi";
 import { useParams } from "react-router-dom";
 
-import { useDagServiceGetDagDetails, useTaskInstanceServiceGetMappedTaskInstance } from "openapi/queries";
+import {
+  useDagServiceGetDagDetails,
+  useGridServiceGridData,
+  useTaskInstanceServiceGetMappedTaskInstance,
+} from "openapi/queries";
 import { DetailsLayout } from "src/layouts/Details/DetailsLayout";
 import { isStatePending, useAutoRefresh } from "src/utils";
 
@@ -67,9 +71,42 @@ export const TaskInstance = () => {
     },
   );
 
+  // Filter grid data to get only a single dag run
+  const { data } = useGridServiceGridData(
+    {
+      dagId,
+      limit: 1,
+      offset: 0,
+      runAfterGte: taskInstance?.run_after,
+      runAfterLte: taskInstance?.run_after,
+    },
+    undefined,
+    {
+      enabled: taskInstance !== undefined,
+    },
+  );
+
+  const mappedTaskInstance = data?.dag_runs
+    .find((dr) => dr.dag_run_id === runId)
+    ?.task_instances.find((ti) => ti.task_id === taskId);
+
+  let newTabs = tabs;
+
+  if (taskInstance && taskInstance.map_index > -1) {
+    newTabs = [
+      ...tabs.slice(0, 1),
+      {
+        icon: <MdOutlineTask />,
+        label: `Task Instances [${mappedTaskInstance?.task_count ?? ""}]`,
+        value: "task_instances",
+      },
+      ...tabs.slice(1),
+    ];
+  }
+
   return (
     <ReactFlowProvider>
-      <DetailsLayout dag={dag} error={error ?? dagError} isLoading={isLoading || isDagLoading} tabs={tabs}>
+      <DetailsLayout dag={dag} error={error ?? dagError} isLoading={isLoading || isDagLoading} tabs={newTabs}>
         {taskInstance === undefined ? undefined : (
           <Header
             isRefreshing={Boolean(isStatePending(taskInstance.state) && Boolean(refetchInterval))}


(airflow) 11/25: Restore hover+selection crosshairs to grid view (#50050)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit d5812660b1e34e9d67dfec97822f1d9ba853564d
Author: Brent Bovenzi <br...@astronomer.io>
AuthorDate: Thu May 1 11:32:21 2025 -0400

    Restore hover+selection crosshairs to grid view (#50050)
    
    (cherry picked from commit 359447665af41b43913f8cf7a22d9b9b353a7cf9)
---
 .../airflow/ui/src/layouts/Details/Grid/Grid.tsx   |   2 +-
 .../airflow/ui/src/layouts/Details/Grid/GridTI.tsx | 105 +++++++++++----------
 .../ui/src/layouts/Details/Grid/TaskNames.tsx      |  22 ++++-
 3 files changed, 76 insertions(+), 53 deletions(-)

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 ba4ecad0b01..75fe7209dc8 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
@@ -72,7 +72,7 @@ export const Grid = ({ limit }: Props) => {
 
   return (
     <Flex justifyContent="flex-start" position="relative" pt={50} width="100%">
-      <Box flexGrow={1} minWidth={7} mr={3} position="relative" top="100px">
+      <Box flexGrow={1} minWidth={7} position="relative" top="100px">
         <TaskNames nodes={flatNodes} />
       </Box>
       <Box position="relative">
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
index 85a8342d8ac..08a17ea9e75 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
@@ -19,7 +19,7 @@
 import { Badge, Flex } from "@chakra-ui/react";
 import type { MouseEvent } from "react";
 import React from "react";
-import { Link } from "react-router-dom";
+import { Link, useParams } from "react-router-dom";
 
 import type { TaskInstanceState } from "openapi/requests/types.gen";
 import { StateIcon } from "src/components/StateIcon";
@@ -36,7 +36,7 @@ type Props = {
 };
 
 const onMouseEnter = (event: MouseEvent<HTMLDivElement>) => {
-  const tasks = document.querySelectorAll<HTMLDivElement>(`#name-${event.currentTarget.id}`);
+  const tasks = document.querySelectorAll<HTMLDivElement>(`#${event.currentTarget.id}`);
 
   tasks.forEach((task) => {
     task.style.backgroundColor = "var(--chakra-colors-blue-subtle)";
@@ -44,56 +44,32 @@ const onMouseEnter = (event: MouseEvent<HTMLDivElement>) => {
 };
 
 const onMouseLeave = (event: MouseEvent<HTMLDivElement>) => {
-  const tasks = document.querySelectorAll<HTMLDivElement>(`#name-${event.currentTarget.id}`);
+  const tasks = document.querySelectorAll<HTMLDivElement>(`#${event.currentTarget.id}`);
 
   tasks.forEach((task) => {
     task.style.backgroundColor = "";
   });
 };
 
-const Instance = ({ dagId, isGroup, isMapped, runId, search, state, taskId }: Props) => (
-  <Flex
-    alignItems="center"
-    height="20px"
-    id={taskId.replaceAll(".", "-")}
-    justifyContent="center"
-    key={taskId}
-    onMouseEnter={onMouseEnter}
-    onMouseLeave={onMouseLeave}
-    px="2px"
-    py={0}
-    transition="background-color 0.2s"
-    zIndex={1}
-  >
-    {isGroup ? (
-      <Badge
-        borderRadius={4}
-        colorPalette={state === null ? "none" : state}
-        height="14px"
-        minH={0}
-        opacity={state === "success" ? 0.6 : 1}
-        p={0}
-        variant="solid"
-        width="14px"
-      >
-        {state === undefined ? undefined : (
-          <StateIcon
-            size={10}
-            state={state}
-            style={{
-              marginLeft: "2px",
-            }}
-          />
-        )}
-      </Badge>
-    ) : (
-      <Link
-        replace
-        to={{
-          pathname: `/dags/${dagId}/runs/${runId}/tasks/${taskId}${isMapped ? "/mapped" : ""}`,
-          search,
-        }}
-      >
+const Instance = ({ dagId, isGroup, isMapped, runId, search, state, taskId }: Props) => {
+  const { taskId: selectedTaskId } = useParams();
+
+  return (
+    <Flex
+      alignItems="center"
+      bg={selectedTaskId === taskId ? "blue.muted" : undefined}
+      height="20px"
+      id={taskId.replaceAll(".", "-")}
+      justifyContent="center"
+      key={taskId}
+      onMouseEnter={onMouseEnter}
+      onMouseLeave={onMouseLeave}
+      px="2px"
+      py={0}
+      transition="background-color 0.2s"
+      zIndex={1}
+    >
+      {isGroup ? (
         <Badge
           borderRadius={4}
           colorPalette={state === null ? "none" : state}
@@ -114,9 +90,38 @@ const Instance = ({ dagId, isGroup, isMapped, runId, search, state, taskId }: Pr
             />
           )}
         </Badge>
-      </Link>
-    )}
-  </Flex>
-);
+      ) : (
+        <Link
+          replace
+          to={{
+            pathname: `/dags/${dagId}/runs/${runId}/tasks/${taskId}${isMapped ? "/mapped" : ""}`,
+            search,
+          }}
+        >
+          <Badge
+            borderRadius={4}
+            colorPalette={state === null ? "none" : state}
+            height="14px"
+            minH={0}
+            opacity={state === "success" ? 0.6 : 1}
+            p={0}
+            variant="solid"
+            width="14px"
+          >
+            {state === undefined ? undefined : (
+              <StateIcon
+                size={10}
+                state={state}
+                style={{
+                  marginLeft: "2px",
+                }}
+              />
+            )}
+          </Badge>
+        </Link>
+      )}
+    </Flex>
+  );
+};
 
 export const GridTI = React.memo(Instance);
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskNames.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskNames.tsx
index 4fe7420c8a9..60105579223 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskNames.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskNames.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { Box, chakra, Flex, Link } from "@chakra-ui/react";
+import type { MouseEvent } from "react";
 import { FiChevronUp } from "react-icons/fi";
 import { Link as RouterLink, useParams, useSearchParams } from "react-router-dom";
 
@@ -30,6 +31,22 @@ type Props = {
   nodes: Array<GridTask>;
 };
 
+const onMouseEnter = (event: MouseEvent<HTMLDivElement>) => {
+  const tasks = document.querySelectorAll<HTMLDivElement>(`#${event.currentTarget.id}`);
+
+  tasks.forEach((task) => {
+    task.style.backgroundColor = "var(--chakra-colors-blue-subtle)";
+  });
+};
+
+const onMouseLeave = (event: MouseEvent<HTMLDivElement>) => {
+  const tasks = document.querySelectorAll<HTMLDivElement>(`#${event.currentTarget.id}`);
+
+  tasks.forEach((task) => {
+    task.style.backgroundColor = "";
+  });
+};
+
 export const TaskNames = ({ nodes }: Props) => {
   const { toggleGroupId } = useOpenGroups();
   const { dagId = "", taskId } = useParams();
@@ -37,13 +54,14 @@ export const TaskNames = ({ nodes }: Props) => {
 
   return nodes.map((node) => (
     <Box
-      _hover={{ bg: "blue.subtle" }}
       bg={node.id === taskId ? "blue.muted" : undefined}
       borderBottomWidth={1}
       borderColor={node.isGroup ? "border.emphasized" : "border.muted"}
-      id={`name-${node.id.replaceAll(".", "-")}`}
+      id={node.id.replaceAll(".", "-")}
       key={node.id}
       maxHeight="20px"
+      onMouseEnter={onMouseEnter}
+      onMouseLeave={onMouseLeave}
       transition="background-color 0.2s"
     >
       {node.isGroup ? (


(airflow) 08/25: Add dateInterval validation and error handling (#50072)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit b174882b9da306cec6baf62790d0d632d499b8b0
Author: Guan Ming(Wesley) Chiu <10...@users.noreply.github.com>
AuthorDate: Thu May 1 20:43:35 2025 +0800

    Add dateInterval validation and error handling (#50072)
    
    (cherry picked from commit 411aa06f66e5afed6576a6416e67c302033bc5bb)
---
 .../src/components/DagActions/RunBackfillForm.tsx  | 48 +++++++++-------------
 1 file changed, 20 insertions(+), 28 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
index 2079de6c604..23162df0453 100644
--- a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { Input, Box, Spacer, HStack, Field, VStack, Flex, Text, Skeleton } from "@chakra-ui/react";
+import dayjs from "dayjs";
 import { useEffect, useState } from "react";
 import { useForm, Controller, useWatch } from "react-hook-form";
 
@@ -100,6 +101,8 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
 
   const dataIntervalStart = watch("from_date");
   const dataIntervalEnd = watch("to_date");
+  const noDataInterval = !Boolean(dataIntervalStart) || !Boolean(dataIntervalEnd);
+  const dataIntervalInvalid = dayjs(dataIntervalStart).isAfter(dayjs(dataIntervalEnd));
 
   const onSubmit = (fdata: BackfillFormProps) => {
     if (unpause && dag.is_paused) {
@@ -132,18 +135,17 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
     total_entries: 0,
   };
 
-  const inlineMessage =
-    !Boolean(values.from_date) || !Boolean(values.to_date) ? undefined : isPendingDryRun ? (
-      <Skeleton height="20px" width="100px" />
-    ) : affectedTasks.total_entries > 0 ? (
-      <Text color="fg.success" fontSize="sm">
-        {pluralize("run", affectedTasks.total_entries)} will be triggered
-      </Text>
-    ) : (
-      <Text color="fg.error" fontSize="sm" fontWeight="medium">
-        No runs matching selected criteria.
-      </Text>
-    );
+  const inlineMessage = isPendingDryRun ? (
+    <Skeleton height="20px" width="100px" />
+  ) : affectedTasks.total_entries > 0 ? (
+    <Text color="fg.success" fontSize="sm">
+      {pluralize("run", affectedTasks.total_entries)} will be triggered
+    </Text>
+  ) : (
+    <Text color="fg.error" fontSize="sm" fontWeight="medium">
+      No runs matching selected criteria.
+    </Text>
+  );
 
   return (
     <>
@@ -158,14 +160,10 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
               control={control}
               name="from_date"
               render={({ field }) => (
-                <Field.Root invalid={Boolean(errors.date)}>
+                <Field.Root invalid={Boolean(errors.date) || dataIntervalInvalid} required>
                   <Field.Label>From</Field.Label>
-                  <DateTimeInput
-                    {...field}
-                    max={dataIntervalEnd || today}
-                    onBlur={resetDateError}
-                    size="sm"
-                  />
+                  <DateTimeInput {...field} max={today} onBlur={resetDateError} size="sm" />
+                  <Field.ErrorText>Start Date must be before the End Date</Field.ErrorText>
                 </Field.Root>
               )}
             />
@@ -173,21 +171,15 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
               control={control}
               name="to_date"
               render={({ field }) => (
-                <Field.Root invalid={Boolean(errors.date)}>
+                <Field.Root invalid={Boolean(errors.date) || dataIntervalInvalid} required>
                   <Field.Label>To</Field.Label>
-                  <DateTimeInput
-                    {...field}
-                    max={today}
-                    min={dataIntervalStart || undefined}
-                    onBlur={resetDateError}
-                    size="sm"
-                  />
+                  <DateTimeInput {...field} max={today} onBlur={resetDateError} size="sm" />
                 </Field.Root>
               )}
             />
           </HStack>
         </Box>
-        <Box>{inlineMessage}</Box>
+        {noDataInterval || dataIntervalInvalid ? undefined : <Box>{inlineMessage}</Box>}
         <Spacer />
         <Controller
           control={control}


(airflow) 14/25: Fix node selections (#50095)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 8490b2c3ca311aa86e6ee82a94e93efa977da950
Author: Brent Bovenzi <br...@astronomer.io>
AuthorDate: Thu May 1 18:32:17 2025 -0400

    Fix node selections (#50095)
    
    (cherry picked from commit 4ece2df42edaa0dd3dd106be3c18b82ffbff3632)
---
 airflow-core/src/airflow/ui/src/components/Graph/DagNode.tsx     | 3 ++-
 airflow-core/src/airflow/ui/src/components/Graph/DefaultNode.tsx | 1 +
 airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx  | 2 +-
 airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx       | 2 +-
 4 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/Graph/DagNode.tsx b/airflow-core/src/airflow/ui/src/components/Graph/DagNode.tsx
index 1b53b4f0359..3cd9e100a4c 100644
--- a/airflow-core/src/airflow/ui/src/components/Graph/DagNode.tsx
+++ b/airflow-core/src/airflow/ui/src/components/Graph/DagNode.tsx
@@ -36,13 +36,14 @@ export const DagNode = ({
     <NodeWrapper>
       <Flex
         bg={isOpen ? "bg.muted" : "bg"}
+        borderColor={isSelected ? "border.inverted" : "border"}
         borderRadius={5}
         borderWidth={isSelected ? 4 : 2}
         cursor="default"
         flexDirection="column"
         height={`${height}px`}
         px={3}
-        py={isSelected ? 0 : 1}
+        py={1}
         width={`${width}px`}
       >
         <HStack alignItems="center" justifyContent="space-between">
diff --git a/airflow-core/src/airflow/ui/src/components/Graph/DefaultNode.tsx b/airflow-core/src/airflow/ui/src/components/Graph/DefaultNode.tsx
index d9e809d517a..5d3813c7d55 100644
--- a/airflow-core/src/airflow/ui/src/components/Graph/DefaultNode.tsx
+++ b/airflow-core/src/airflow/ui/src/components/Graph/DefaultNode.tsx
@@ -28,6 +28,7 @@ export const DefaultNode = ({ data: { height, label, width } }: NodeProps<NodeTy
       bg="bg"
       borderRadius={5}
       borderWidth={2}
+      cursor="auto"
       flexDirection="column"
       height={`${height}px`}
       p={2}
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx
index 12744c7d15f..89a11ad11a9 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx
@@ -144,7 +144,7 @@ export const Graph = () => {
       ...node,
       data: {
         ...node.data,
-        isSelected: node.id === taskId,
+        isSelected: node.id === taskId || node.id === `dag:${dagId}`,
         taskInstance,
       },
     };
diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx
index 42e1c39071f..94055eadcde 100644
--- a/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx
@@ -42,7 +42,7 @@ export const AssetGraph = ({ asset }: { readonly asset?: AssetResponse }) => {
   });
 
   const nodes = graphData?.nodes.map((node) =>
-    node.data.label === asset?.name ? { ...node, data: { ...node.data, isSelected: true } } : node,
+    node.id === `asset:${assetId}` ? { ...node, data: { ...node.data, isSelected: true } } : node,
   );
 
   const [selectedDarkColor, selectedLightColor] = useToken("colors", ["gray.200", "gray.800"]);


(airflow) 02/25: Fix ContinuousTimetable false triggering when last run ends in future (#45175)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit f2cae5f3c4caef466025fd2e27c8886fc2b76665
Author: jj.lee <63...@users.noreply.github.com>
AuthorDate: Thu May 1 01:59:27 2025 +0900

    Fix ContinuousTimetable false triggering when last run ends in future (#45175)
    
    closes: #45081
    (cherry picked from commit 0e846a4520cd0a33cb73f22b1404eb01cad4b561)
---
 airflow-core/src/airflow/timetables/simple.py           | 17 ++++++++++++-----
 .../tests/unit/timetables/test_continuous_timetable.py  | 11 +++++++++++
 2 files changed, 23 insertions(+), 5 deletions(-)

diff --git a/airflow-core/src/airflow/timetables/simple.py b/airflow-core/src/airflow/timetables/simple.py
index 49dcda81a4f..0dfcd6060de 100644
--- a/airflow-core/src/airflow/timetables/simple.py
+++ b/airflow-core/src/airflow/timetables/simple.py
@@ -135,14 +135,21 @@ class ContinuousTimetable(_TrivialTimetable):
     ) -> DagRunInfo | None:
         if restriction.earliest is None:  # No start date, won't run.
             return None
+
+        current_time = timezone.coerce_datetime(timezone.utcnow())
+
         if last_automated_data_interval is not None:  # has already run once
-            start = last_automated_data_interval.end
-            end = timezone.coerce_datetime(timezone.utcnow())
+            if last_automated_data_interval.end > current_time:  # start date is future
+                start = restriction.earliest
+                elapsed = last_automated_data_interval.end - last_automated_data_interval.start
+
+                end = start + elapsed.as_timedelta()
+            else:
+                start = last_automated_data_interval.end
+                end = current_time
         else:  # first run
             start = restriction.earliest
-            end = max(
-                restriction.earliest, timezone.coerce_datetime(timezone.utcnow())
-            )  # won't run any earlier than start_date
+            end = max(restriction.earliest, current_time)
 
         if restriction.latest is not None and end > restriction.latest:
             return None
diff --git a/airflow-core/tests/unit/timetables/test_continuous_timetable.py b/airflow-core/tests/unit/timetables/test_continuous_timetable.py
index 767dc251925..73a4be5ea48 100644
--- a/airflow-core/tests/unit/timetables/test_continuous_timetable.py
+++ b/airflow-core/tests/unit/timetables/test_continuous_timetable.py
@@ -89,3 +89,14 @@ def test_no_runs_after_end_date(timetable, restriction):
     )
 
     assert next_info is None
+
+
+@time_machine.travel(BEFORE_DATE)
+def test_no_false_triggering_with_future_start_date_after_run(timetable, restriction):
+    next_info = timetable.next_dagrun_info(
+        last_automated_data_interval=DataInterval(BEFORE_DATE, BEFORE_DATE.add(hours=1)),
+        restriction=restriction,
+    )
+
+    assert next_info is not None
+    assert next_info.data_interval.start == START_DATE


(airflow) 04/25: minor cosmetic changes (#50044)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 9a43eb8d7362137df0aa14f6ad5b9c8f1d4abfa9
Author: Aritra Basu <24...@users.noreply.github.com>
AuthorDate: Wed Apr 30 23:42:44 2025 +0530

    minor cosmetic changes (#50044)
    
    (cherry picked from commit 01e139e6c454e811b4da08034b8133f5daecf647)
---
 airflow-core/src/airflow/ui/src/components/DataTable/DataTable.tsx | 6 +++++-
 airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx      | 2 +-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.tsx b/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.tsx
index 785f04c0d68..fcd4a80c109 100644
--- a/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.tsx
@@ -142,7 +142,11 @@ export const DataTable = <TData,>({
       {hasRows && display === "card" && cardDef !== undefined ? (
         <CardList cardDef={cardDef} isLoading={isLoading} table={table} />
       ) : undefined}
-      {!hasRows && !Boolean(isLoading) && <Text pt={1}>{noRowsMessage ?? `No ${modelName}s found.`}</Text>}
+      {!hasRows && !Boolean(isLoading) && (
+        <Text pl={4} pt={1}>
+          {noRowsMessage ?? `No ${modelName}s found.`}
+        </Text>
+      )}
       {hasPagination ? (
         <Pagination.Root
           count={table.getRowCount()}
diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx
index cad4f8aa985..7e3ca81985e 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx
@@ -55,7 +55,7 @@ export const Dashboard = () => {
           </Accordion.Root>
         ) : undefined}
         <Heading mb={2} size="2xl">
-          Welcome,
+          Welcome
         </Heading>
       </VStack>
       <Box>


(airflow) 10/25: Added validation in flexibleform (#49981)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 848c1b31140bf7ebc728f5e58306519f994688e9
Author: Aritra Basu <24...@users.noreply.github.com>
AuthorDate: Thu May 1 20:37:25 2025 +0530

    Added validation in flexibleform (#49981)
    
    * Added validation in flexibleform
    
    * Updated errors
    
    * Added more validation and disabling button
    
    * Fixed boolean error
    
    * Makes section red on error
    
    (cherry picked from commit 607ff45fa00812b86c52e62dbb3a3229f169ed5d)
---
 .../src/airflow/ui/src/components/ConfigForm.tsx   |  3 ++
 .../src/components/DagActions/RunBackfillForm.tsx  |  6 ++-
 .../components/FlexibleForm/FieldAdvancedArray.tsx | 27 ++++-------
 .../src/components/FlexibleForm/FieldDateTime.tsx  |  4 +-
 .../src/components/FlexibleForm/FieldDropdown.tsx  |  3 +-
 .../components/FlexibleForm/FieldMultiSelect.tsx   |  3 +-
 .../components/FlexibleForm/FieldMultilineText.tsx |  3 +-
 .../ui/src/components/FlexibleForm/FieldNumber.tsx |  3 +-
 .../ui/src/components/FlexibleForm/FieldObject.tsx | 23 ++++------
 .../ui/src/components/FlexibleForm/FieldRow.tsx    | 38 +++++++++++-----
 .../src/components/FlexibleForm/FieldSelector.tsx  | 26 +++++------
 .../ui/src/components/FlexibleForm/FieldString.tsx |  3 +-
 .../components/FlexibleForm/FieldStringArray.tsx   |  3 +-
 .../src/components/FlexibleForm/FlexibleForm.tsx   | 52 ++++++++++++++++++++--
 .../airflow/ui/src/components/FlexibleForm/Row.tsx |  8 +++-
 .../ui/src/components/FlexibleForm/index.tsx       |  1 +
 .../{index.tsx => isParamRequired.tsx}             | 14 +++---
 .../src/components/TriggerDag/TriggerDAGForm.tsx   |  4 +-
 .../ui/src/pages/Connections/ConnectionForm.tsx    |  5 ++-
 19 files changed, 148 insertions(+), 81 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx
index a216b22de60..c8e75edfa81 100644
--- a/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx
@@ -39,6 +39,7 @@ type ConfigFormProps<T extends FieldValues = FieldValues> = {
       date?: unknown;
     }>
   >;
+  readonly setFormError: (error: boolean) => void;
 };
 
 const ConfigForm = <T extends FieldValues = FieldValues>({
@@ -47,6 +48,7 @@ const ConfigForm = <T extends FieldValues = FieldValues>({
   errors,
   initialParamsDict,
   setErrors,
+  setFormError,
 }: ConfigFormProps<T>) => {
   const { conf, setConf } = useParamStore();
 
@@ -86,6 +88,7 @@ const ConfigForm = <T extends FieldValues = FieldValues>({
       <FlexibleForm
         flexibleFormDefaultSection={flexibleFormDefaultSection}
         initialParamsDict={initialParamsDict}
+        setError={setFormError}
       />
       <Accordion.Item key="advancedOptions" value="advancedOptions">
         <Accordion.ItemTrigger cursor="button">Advanced Options</Accordion.ItemTrigger>
diff --git a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
index 23162df0453..b86aa824c9b 100644
--- a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
@@ -49,6 +49,7 @@ type BackfillFormProps = DagRunTriggerParams & Omit<BackfillPostBody, "dag_run_c
 const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
   const [errors, setErrors] = useState<{ conf?: string; date?: unknown }>({});
   const [unpause, setUnpause] = useState(true);
+  const [formError, setFormError] = useState(false);
   const initialParamsDict = useDagParams(dag.dag_id, true);
   const { conf } = useParamStore();
   const { control, handleSubmit, reset, watch } = useForm<BackfillFormProps>({
@@ -251,6 +252,7 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
           errors={errors}
           initialParamsDict={initialParamsDict}
           setErrors={setErrors}
+          setFormError={setFormError}
         />
       </VStack>
       <Box as="footer" display="flex" justifyContent="flex-end" mt={4}>
@@ -259,7 +261,9 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
           <Button onClick={() => void handleSubmit(onCancel)()}>Cancel</Button>
           <Button
             colorPalette="blue"
-            disabled={Boolean(errors.date) || isPendingDryRun || affectedTasks.total_entries === 0}
+            disabled={
+              Boolean(errors.date) || isPendingDryRun || formError || affectedTasks.total_entries === 0
+            }
             loading={isPending}
             onClick={() => void handleSubmit(onSubmit)()}
           >
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx
index 80cb46dbdbc..27221204665 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx
@@ -16,23 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Text } from "@chakra-ui/react";
-import { useState } from "react";
-
 import { paramPlaceholder, useParamStore } from "src/queries/useParamStore";
 
 import type { FlexibleFormElementProps } from ".";
 import { JsonEditor } from "../JsonEditor";
 
-export const FieldAdvancedArray = ({ name }: FlexibleFormElementProps) => {
+export const FieldAdvancedArray = ({ name, onUpdate }: FlexibleFormElementProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
-  const [error, setError] = useState<unknown>(undefined);
   // Determine the expected type based on schema
   const expectedType = param.schema.items?.type ?? "object";
 
   const handleChange = (value: string) => {
-    setError(undefined);
     if (value === "") {
       if (paramsDict[name]) {
         // "undefined" values are removed from params, so we set it to null to avoid falling back to DAG defaults.
@@ -64,24 +59,18 @@ export const FieldAdvancedArray = ({ name }: FlexibleFormElementProps) => {
         }
 
         setParamsDict(paramsDict);
+        onUpdate(String(parsedValue));
       } catch (_error) {
-        setError(expectedType === "number" ? String(_error).replace("JSON", "Array") : _error);
+        onUpdate(undefined, expectedType === "number" ? String(_error).replace("JSON", "Array") : _error);
       }
     }
   };
 
   return (
-    <>
-      <JsonEditor
-        id={`element_${name}`}
-        onChange={handleChange}
-        value={JSON.stringify(param.value ?? [], undefined, 2)}
-      />
-      {Boolean(error) ? (
-        <Text color="fg.error" fontSize="xs">
-          {String(error)}
-        </Text>
-      ) : undefined}
-    </>
+    <JsonEditor
+      id={`element_${name}`}
+      onChange={handleChange}
+      value={JSON.stringify(param.value ?? [], undefined, 2)}
+    />
   );
 };
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx
index 07911837996..cc8d23184b3 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx
@@ -23,7 +23,7 @@ import { paramPlaceholder, useParamStore } from "src/queries/useParamStore";
 import type { FlexibleFormElementProps } from ".";
 import { DateTimeInput } from "../DateTimeInput";
 
-export const FieldDateTime = ({ name, ...rest }: FlexibleFormElementProps & InputProps) => {
+export const FieldDateTime = ({ name, onUpdate, ...rest }: FlexibleFormElementProps & InputProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
   const handleChange = (value: string) => {
@@ -40,6 +40,7 @@ export const FieldDateTime = ({ name, ...rest }: FlexibleFormElementProps & Inpu
     }
 
     setParamsDict(paramsDict);
+    onUpdate(value);
   };
 
   if (rest.type === "datetime-local") {
@@ -59,6 +60,7 @@ export const FieldDateTime = ({ name, ...rest }: FlexibleFormElementProps & Inpu
       id={`element_${name}`}
       name={`element_${name}`}
       onChange={(event) => handleChange(event.target.value)}
+      required={rest.required}
       size="sm"
       type={rest.type}
       value={((param.value ?? "") as string).slice(0, 16)}
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx
index a12d4dc366d..de15568c39c 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx
@@ -33,7 +33,7 @@ const labelLookup = (key: string, valuesDisplay: Record<string, string> | undefi
 };
 const enumTypes = ["string", "number", "integer"];
 
-export const FieldDropdown = ({ name }: FlexibleFormElementProps) => {
+export const FieldDropdown = ({ name, onUpdate }: FlexibleFormElementProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
 
@@ -55,6 +55,7 @@ export const FieldDropdown = ({ name }: FlexibleFormElementProps) => {
     }
 
     setParamsDict(paramsDict);
+    onUpdate(value);
   };
 
   return (
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx
index aa4a4e112f9..e34241037e1 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx
@@ -31,7 +31,7 @@ const labelLookup = (key: string, valuesDisplay: Record<string, string> | undefi
   return key;
 };
 
-export const FieldMultiSelect = ({ name }: FlexibleFormElementProps) => {
+export const FieldMultiSelect = ({ name, onUpdate }: FlexibleFormElementProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
 
@@ -64,6 +64,7 @@ export const FieldMultiSelect = ({ name }: FlexibleFormElementProps) => {
       paramsDict[name].value = newValueArray;
     }
     setParamsDict(paramsDict);
+    onUpdate(String(newValueArray));
   };
 
   return (
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultilineText.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultilineText.tsx
index bfefcb276d8..93c7410bb82 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultilineText.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultilineText.tsx
@@ -22,7 +22,7 @@ import { paramPlaceholder, useParamStore } from "src/queries/useParamStore";
 
 import type { FlexibleFormElementProps } from ".";
 
-export const FieldMultilineText = ({ name }: FlexibleFormElementProps) => {
+export const FieldMultilineText = ({ name, onUpdate }: FlexibleFormElementProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
   const handleChange = (value: string) => {
@@ -33,6 +33,7 @@ export const FieldMultilineText = ({ name }: FlexibleFormElementProps) => {
     }
 
     setParamsDict(paramsDict);
+    onUpdate(value);
   };
 
   return (
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldNumber.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldNumber.tsx
index dcaaf37cf56..635f461335c 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldNumber.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldNumber.tsx
@@ -21,7 +21,7 @@ import { paramPlaceholder, useParamStore } from "src/queries/useParamStore";
 import type { FlexibleFormElementProps } from ".";
 import { NumberInputField, NumberInputRoot } from "../ui/NumberInput";
 
-export const FieldNumber = ({ name }: FlexibleFormElementProps) => {
+export const FieldNumber = ({ name, onUpdate }: FlexibleFormElementProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
   const handleChange = (value: string) => {
@@ -40,6 +40,7 @@ export const FieldNumber = ({ name }: FlexibleFormElementProps) => {
     }
 
     setParamsDict(paramsDict);
+    onUpdate(value);
   };
 
   return (
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldObject.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldObject.tsx
index fcd7d7d2a16..ce5bf307311 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldObject.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldObject.tsx
@@ -16,21 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Text } from "@chakra-ui/react";
-import { useState } from "react";
-
 import { paramPlaceholder, useParamStore } from "src/queries/useParamStore";
 
 import type { FlexibleFormElementProps } from ".";
 import { JsonEditor } from "../JsonEditor";
 
-export const FieldObject = ({ name }: FlexibleFormElementProps) => {
+export const FieldObject = ({ name, onUpdate }: FlexibleFormElementProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
-  const [error, setError] = useState<unknown>(undefined);
 
   const handleChange = (value: string) => {
-    setError(undefined);
     try {
       // "undefined" values are removed from params, so we set it to null to avoid falling back to DAG defaults.
       // eslint-disable-next-line unicorn/no-null
@@ -41,19 +36,17 @@ export const FieldObject = ({ name }: FlexibleFormElementProps) => {
       }
 
       setParamsDict(paramsDict);
+      onUpdate(value);
     } catch (_error) {
-      setError(_error);
+      onUpdate("", _error);
     }
   };
 
   return (
-    <>
-      <JsonEditor
-        id={`element_${name}`}
-        onChange={handleChange}
-        value={JSON.stringify(param.value ?? [], undefined, 2)}
-      />
-      {Boolean(error) ? <Text color="fg,.error">{String(error)}</Text> : undefined}
-    </>
+    <JsonEditor
+      id={`element_${name}`}
+      onChange={handleChange}
+      value={JSON.stringify(param.value ?? [], undefined, 2)}
+    />
   );
 };
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldRow.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldRow.tsx
index e41d4643f6b..406dbaf18fe 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldRow.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldRow.tsx
@@ -17,35 +17,52 @@
  * under the License.
  */
 import { Field, Stack } from "@chakra-ui/react";
+import { useState } from "react";
 import Markdown from "react-markdown";
 import remarkGfm from "remark-gfm";
 
-import type { ParamSpec } from "src/queries/useDagParams";
 import { paramPlaceholder, useParamStore } from "src/queries/useParamStore";
 
 import type { FlexibleFormElementProps } from ".";
 import { FieldSelector } from "./FieldSelector";
-
-const isRequired = (param: ParamSpec) =>
-  // The field is required if the schema type is defined.
-  // But if the type "null" is included, then the field is not required.
-  // We assume that "null" is only defined if the type is an array.
-  Boolean(param.schema.type) && (!Array.isArray(param.schema.type) || !param.schema.type.includes("null"));
+import { isRequired } from "./isParamRequired";
 
 /** Render a normal form row with a field that is auto-selected */
-export const FieldRow = ({ name }: FlexibleFormElementProps) => {
+export const FieldRow = ({ name, onUpdate: rowOnUpdate }: FlexibleFormElementProps) => {
   const { paramsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
+  const [error, setError] = useState<unknown>(
+    isRequired(param) && param.value === null ? "This field is required" : undefined,
+  );
+  const [isValid, setIsValid] = useState(!(isRequired(param) && param.value === null));
+
+  // console.log(param);
+
+  const onUpdate = (value?: string, _error?: unknown) => {
+    if (Boolean(_error)) {
+      setIsValid(false);
+      setError(_error);
+      rowOnUpdate(undefined, _error);
+    } else if (isRequired(param) && (!Boolean(value) || value === "")) {
+      setIsValid(false);
+      setError("This field is required");
+      rowOnUpdate(undefined, "This field is required");
+    } else {
+      setIsValid(true);
+      setError(undefined);
+      rowOnUpdate();
+    }
+  };
 
   return (
-    <Field.Root orientation="horizontal" required={isRequired(param)}>
+    <Field.Root invalid={!isValid} orientation="horizontal" required={isRequired(param)}>
       <Stack>
         <Field.Label fontSize="md" style={{ flexBasis: "30%" }}>
           {param.schema.title ?? name} <Field.RequiredIndicator />
         </Field.Label>
       </Stack>
       <Stack css={{ flexBasis: "70%" }}>
-        <FieldSelector name={name} />
+        <FieldSelector name={name} onUpdate={onUpdate} />
         {param.description === null ? (
           param.schema.description_md === undefined ? undefined : (
             <Field.HelperText>
@@ -55,6 +72,7 @@ export const FieldRow = ({ name }: FlexibleFormElementProps) => {
         ) : (
           <Field.HelperText>{param.description}</Field.HelperText>
         )}
+        {isValid ? undefined : <Field.ErrorText>{String(error)}</Field.ErrorText>}
       </Stack>
     </Field.Root>
   );
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldSelector.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldSelector.tsx
index 8665b41aa2b..cd0061987a6 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldSelector.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldSelector.tsx
@@ -87,35 +87,35 @@ const isFieldStringArray = (fieldType: string, fieldSchema: ParamSchema) =>
 const isFieldTime = (fieldType: string, fieldSchema: ParamSchema) =>
   fieldType === "string" && fieldSchema.format === "time";
 
-export const FieldSelector = ({ name }: FlexibleFormElementProps) => {
+export const FieldSelector = ({ name, onUpdate }: FlexibleFormElementProps) => {
   // FUTURE: Add support for other types as described in AIP-68 via Plugins
   const { initialParamDict } = useParamStore();
   const param = initialParamDict[name] ?? paramPlaceholder;
   const fieldType = inferType(param);
 
   if (isFieldBool(fieldType)) {
-    return <FieldBool name={name} />;
+    return <FieldBool name={name} onUpdate={onUpdate} />;
   } else if (isFieldDateTime(fieldType, param.schema)) {
-    return <FieldDateTime name={name} type="datetime-local" />;
+    return <FieldDateTime name={name} onUpdate={onUpdate} type="datetime-local" />;
   } else if (isFieldDate(fieldType, param.schema)) {
-    return <FieldDateTime name={name} type="date" />;
+    return <FieldDateTime name={name} onUpdate={onUpdate} type="date" />;
   } else if (isFieldTime(fieldType, param.schema)) {
-    return <FieldDateTime name={name} type="time" />;
+    return <FieldDateTime name={name} onUpdate={onUpdate} type="time" />;
   } else if (isFieldDropdown(fieldType, param.schema)) {
-    return <FieldDropdown name={name} />;
+    return <FieldDropdown name={name} onUpdate={onUpdate} />;
   } else if (isFieldMultiSelect(fieldType, param.schema)) {
-    return <FieldMultiSelect name={name} />;
+    return <FieldMultiSelect name={name} onUpdate={onUpdate} />;
   } else if (isFieldStringArray(fieldType, param.schema)) {
-    return <FieldStringArray name={name} />;
+    return <FieldStringArray name={name} onUpdate={onUpdate} />;
   } else if (isFieldAdvancedArray(fieldType, param.schema)) {
-    return <FieldAdvancedArray name={name} />;
+    return <FieldAdvancedArray name={name} onUpdate={onUpdate} />;
   } else if (isFieldObject(fieldType)) {
-    return <FieldObject name={name} />;
+    return <FieldObject name={name} onUpdate={onUpdate} />;
   } else if (isFieldNumber(fieldType)) {
-    return <FieldNumber name={name} />;
+    return <FieldNumber name={name} onUpdate={onUpdate} />;
   } else if (isFieldMultilineText(fieldType, param.schema)) {
-    return <FieldMultilineText name={name} />;
+    return <FieldMultilineText name={name} onUpdate={onUpdate} />;
   } else {
-    return <FieldString name={name} />;
+    return <FieldString name={name} onUpdate={onUpdate} />;
   }
 };
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldString.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldString.tsx
index e8cd0185706..8d49e604452 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldString.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldString.tsx
@@ -22,7 +22,7 @@ import { paramPlaceholder, useParamStore } from "src/queries/useParamStore";
 
 import type { FlexibleFormElementProps } from ".";
 
-export const FieldString = ({ name }: FlexibleFormElementProps) => {
+export const FieldString = ({ name, onUpdate }: FlexibleFormElementProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
   const handleChange = (value: string) => {
@@ -33,6 +33,7 @@ export const FieldString = ({ name }: FlexibleFormElementProps) => {
     }
 
     setParamsDict(paramsDict);
+    onUpdate(value);
   };
 
   return (
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldStringArray.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldStringArray.tsx
index f5fc6ac69f5..2078d95276d 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldStringArray.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldStringArray.tsx
@@ -22,7 +22,7 @@ import { paramPlaceholder, useParamStore } from "src/queries/useParamStore";
 
 import type { FlexibleFormElementProps } from ".";
 
-export const FieldStringArray = ({ name }: FlexibleFormElementProps) => {
+export const FieldStringArray = ({ name, onUpdate }: FlexibleFormElementProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
 
@@ -34,6 +34,7 @@ export const FieldStringArray = ({ name }: FlexibleFormElementProps) => {
     }
 
     setParamsDict(paramsDict);
+    onUpdate(newValue);
   };
 
   const handleBlur = () => {
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
index 165e9d6dd19..ae9e60c812f 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
@@ -17,23 +17,44 @@
  * under the License.
  */
 import { Box, Stack, StackSeparator } from "@chakra-ui/react";
-import { useEffect } from "react";
+import { useCallback, useEffect, useState } from "react";
 
 import type { ParamsSpec } from "src/queries/useDagParams";
 import { useParamStore } from "src/queries/useParamStore";
 
 import { Accordion } from "../ui";
 import { Row } from "./Row";
+import { isRequired } from "./isParamRequired";
 
 export type FlexibleFormProps = {
   flexibleFormDefaultSection: string;
   initialParamsDict: { paramsDict: ParamsSpec };
   key?: string;
+  setError: (error: boolean) => void;
 };
 
-export const FlexibleForm = ({ flexibleFormDefaultSection, initialParamsDict }: FlexibleFormProps) => {
+export const FlexibleForm = ({
+  flexibleFormDefaultSection,
+  initialParamsDict,
+  setError,
+}: FlexibleFormProps) => {
   const { paramsDict: params, setinitialParamDict, setParamsDict } = useParamStore();
   const processedSections = new Map();
+  const [sectionError, setSectionError] = useState<Map<string, boolean>>(new Map());
+
+  const recheckSection = useCallback(() => {
+    sectionError.clear();
+    Object.entries(params).forEach(([, element]) => {
+      if (
+        isRequired(element) &&
+        (element.value === null || element.value === undefined || element.value === "")
+      ) {
+        sectionError.set(element.schema.section ?? flexibleFormDefaultSection, true);
+        setSectionError(sectionError);
+      }
+    });
+    console.log("errors1", sectionError);
+  }, [flexibleFormDefaultSection, params, sectionError]);
 
   useEffect(() => {
     // Initialize paramsDict and initialParamDict when modal opens
@@ -54,6 +75,24 @@ export const FlexibleForm = ({ flexibleFormDefaultSection, initialParamsDict }:
     [setParamsDict, setinitialParamDict],
   );
 
+  useEffect(
+    () => () => {
+      recheckSection();
+    },
+    [params, recheckSection],
+  );
+
+  const onUpdate = (_value?: string, error?: unknown) => {
+    recheckSection();
+    if (!Boolean(error) && sectionError.size === 0) {
+      setError(false);
+    } else {
+      setError(true);
+    }
+  };
+
+  console.log(sectionError);
+
   return Object.entries(params).some(([, param]) => typeof param.schema.section !== "string")
     ? Object.entries(params).map(([, secParam]) => {
         const currentSection = secParam.schema.section ?? flexibleFormDefaultSection;
@@ -65,7 +104,12 @@ export const FlexibleForm = ({ flexibleFormDefaultSection, initialParamsDict }:
 
           return (
             <Accordion.Item key={currentSection} value={currentSection}>
-              <Accordion.ItemTrigger cursor="button">{currentSection}</Accordion.ItemTrigger>
+              <Accordion.ItemTrigger
+                color={sectionError.get(currentSection) ? "red" : undefined}
+                cursor="button"
+              >
+                {currentSection}
+              </Accordion.ItemTrigger>
               <Accordion.ItemContent paddingTop={0}>
                 <Box p={5}>
                   <Stack separator={<StackSeparator />}>
@@ -76,7 +120,7 @@ export const FlexibleForm = ({ flexibleFormDefaultSection, initialParamsDict }:
                           (currentSection === flexibleFormDefaultSection && !Boolean(param.schema.section)),
                       )
                       .map(([name]) => (
-                        <Row key={name} name={name} />
+                        <Row key={name} name={name} onUpdate={onUpdate} />
                       ))}
                   </Stack>
                 </Box>
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/Row.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/Row.tsx
index 1f1186124a2..cc71fedb6ee 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/Row.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/Row.tsx
@@ -26,9 +26,13 @@ import { HiddenInput } from "./HiddenInput";
 const isHidden = (fieldSchema: ParamSchema) => Boolean(fieldSchema.const);
 
 /** Generates a form row */
-export const Row = ({ name }: FlexibleFormElementProps) => {
+export const Row = ({ name, onUpdate }: FlexibleFormElementProps) => {
   const { paramsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
 
-  return isHidden(param.schema) ? <HiddenInput name={name} /> : <FieldRow name={name} />;
+  return isHidden(param.schema) ? (
+    <HiddenInput name={name} onUpdate={onUpdate} />
+  ) : (
+    <FieldRow name={name} onUpdate={onUpdate} />
+  );
 };
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx
index f7e8ab0d8c4..704b5d956ae 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx
@@ -19,6 +19,7 @@
 
 export type FlexibleFormElementProps = {
   readonly name: string;
+  readonly onUpdate: (value?: string, error?: unknown) => void;
 };
 
 export const flexibleFormDefaultSection = "Run Parameters";
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/isParamRequired.tsx
similarity index 66%
copy from airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx
copy to airflow-core/src/airflow/ui/src/components/FlexibleForm/isParamRequired.tsx
index f7e8ab0d8c4..7a9a0668855 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/isParamRequired.tsx
@@ -16,12 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import type { ParamSpec } from "src/queries/useDagParams";
 
-export type FlexibleFormElementProps = {
-  readonly name: string;
-};
-
-export const flexibleFormDefaultSection = "Run Parameters";
-export const flexibleFormExtraFieldSection = "Extra Fields";
-
-export { FlexibleForm } from "./FlexibleForm";
+export const isRequired = (param: ParamSpec) =>
+  // The field is required if the schema type is defined.
+  // But if the type "null" is included, then the field is not required.
+  // We assume that "null" is only defined if the type is an array.
+  Boolean(param.schema.type) && (!Array.isArray(param.schema.type) || !param.schema.type.includes("null"));
diff --git a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
index fdeae4bdb69..e1337f530f6 100644
--- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
@@ -49,6 +49,7 @@ export type DagRunTriggerParams = {
 
 const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: TriggerDAGFormProps) => {
   const [errors, setErrors] = useState<{ conf?: string; date?: unknown }>({});
+  const [formError, setFormError] = useState(false);
   const initialParamsDict = useDagParams(dagId, open);
   const { error: errorTrigger, isPending, triggerDagRun } = useTrigger({ dagId, onSuccessConfirm: onClose });
   const { conf } = useParamStore();
@@ -96,6 +97,7 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: TriggerDAGFormProps)
         errors={errors}
         initialParamsDict={initialParamsDict}
         setErrors={setErrors}
+        setFormError={setFormError}
       >
         <Controller
           control={control}
@@ -153,7 +155,7 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: TriggerDAGFormProps)
           <Spacer />
           <Button
             colorPalette="blue"
-            disabled={Boolean(errors.conf) || Boolean(errors.date) || isPending}
+            disabled={Boolean(errors.conf) || Boolean(errors.date) || formError || isPending}
             onClick={() => void handleSubmit(onSubmit)()}
           >
             <FiPlay /> Trigger
diff --git a/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx
index f2a47c47b15..6a17f1794f6 100644
--- a/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx
@@ -68,6 +68,8 @@ const ConnectionForm = ({
   const standardFields = connectionTypeMeta[selectedConnType]?.standard_fields ?? {};
   const paramsDic = { paramsDict: connectionTypeMeta[selectedConnType]?.extra_fields ?? ({} as ParamsSpec) };
 
+  const [formErrors, setFormErrors] = useState(false);
+
   useEffect(() => {
     reset((prevValues) => ({
       ...initialConnection,
@@ -198,6 +200,7 @@ const ConnectionForm = ({
               flexibleFormDefaultSection={flexibleFormExtraFieldSection}
               initialParamsDict={paramsDic}
               key={selectedConnType}
+              setError={setFormErrors}
             />
             <Accordion.Item key="extraJson" value="extraJson">
               <Accordion.ItemTrigger cursor="button">Extra Fields JSON</Accordion.ItemTrigger>
@@ -228,7 +231,7 @@ const ConnectionForm = ({
           <Spacer />
           <Button
             colorPalette="blue"
-            disabled={Boolean(errors.conf) || isPending || !isValid}
+            disabled={Boolean(errors.conf) || formErrors || isPending || !isValid}
             onClick={() => void handleSubmit(onSubmit)()}
           >
             <FiSave /> Save


(airflow) 05/25: refactor: migrate to new ui endpoint (#50028)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 4cc590d559e54bc0aaee16d8e016b2fbdbe90d16
Author: Guan Ming(Wesley) Chiu <10...@users.noreply.github.com>
AuthorDate: Thu May 1 02:16:59 2025 +0800

    refactor: migrate to new ui endpoint (#50028)
    
    (cherry picked from commit 129f07d26f01c0860339b3ec4c893ed5f65c4c35)
---
 .../airflow/ui/src/pages/Dashboard/Stats/Stats.tsx | 35 +++++++---------------
 1 file changed, 10 insertions(+), 25 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
index 085a348fa7a..03149103894 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
@@ -19,32 +19,17 @@
 import { Box, Flex, Heading, HStack } from "@chakra-ui/react";
 import { FiClipboard, FiZap } from "react-icons/fi";
 
-import { useDagServiceGetDags } from "openapi/queries";
+import { useDashboardServiceDagStats } from "openapi/queries";
 
 import { DAGImportErrors } from "./DAGImportErrors";
 import { StatsCard } from "./StatsCard";
 
 export const Stats = () => {
-  const { data: activeDagsData, isLoading: isActiveDagsLoading } = useDagServiceGetDags({
-    paused: false,
-  });
-
-  const { data: failedDagsData, isLoading: isFailedDagsLoading } = useDagServiceGetDags({
-    lastDagRunState: "failed",
-  });
-
-  const { data: queuedDagsData, isLoading: isQueuedDagsLoading } = useDagServiceGetDags({
-    lastDagRunState: "queued",
-  });
-
-  const { data: runningDagsData, isLoading: isRunningDagsLoading } = useDagServiceGetDags({
-    lastDagRunState: "running",
-  });
-
-  const activeDagsCount = activeDagsData?.total_entries ?? 0;
-  const failedDagsCount = failedDagsData?.total_entries ?? 0;
-  const queuedDagsCount = queuedDagsData?.total_entries ?? 0;
-  const runningDagsCount = runningDagsData?.total_entries ?? 0;
+  const { data: statsData, isLoading: isStatsLoading } = useDashboardServiceDagStats();
+  const failedDagsCount = statsData?.failed_dag_count ?? 0;
+  const queuedDagsCount = statsData?.queued_dag_count ?? 0;
+  const runningDagsCount = statsData?.running_dag_count ?? 0;
+  const activeDagsCount = statsData?.active_dag_count ?? 0;
 
   return (
     <Box>
@@ -59,7 +44,7 @@ export const Stats = () => {
         <StatsCard
           colorScheme="failed"
           count={failedDagsCount}
-          isLoading={isFailedDagsLoading}
+          isLoading={isStatsLoading}
           label="Failed dags"
           link="dags?last_dag_run_state=failed"
           state="failed"
@@ -71,7 +56,7 @@ export const Stats = () => {
           <StatsCard
             colorScheme="queued"
             count={queuedDagsCount}
-            isLoading={isQueuedDagsLoading}
+            isLoading={isStatsLoading}
             label="Queued dags"
             link="dags?last_dag_run_state=queued"
             state="queued"
@@ -81,7 +66,7 @@ export const Stats = () => {
         <StatsCard
           colorScheme="running"
           count={runningDagsCount}
-          isLoading={isRunningDagsLoading}
+          isLoading={isStatsLoading}
           label="Running dags"
           link="dags?last_dag_run_state=running"
           state="running"
@@ -91,7 +76,7 @@ export const Stats = () => {
           colorScheme="blue"
           count={activeDagsCount}
           icon={<FiZap />}
-          isLoading={isActiveDagsLoading}
+          isLoading={isStatsLoading}
           label="Active dags"
           link="dags?paused=false"
         />


(airflow) 23/25: Use top-level imports in docs if possible (#50195)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 746257fc105064a5cd249de4ed17f5bc95f85113
Author: Tzu-ping Chung <ur...@gmail.com>
AuthorDate: Mon May 5 17:58:51 2025 +0800

    Use top-level imports in docs if possible (#50195)
    
    (cherry picked from commit c5cfe4883ee9a3b4c596cdc79ebf7b2097bcd03b)
---
 airflow-core/docs/authoring-and-scheduling/assets.rst | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/airflow-core/docs/authoring-and-scheduling/assets.rst b/airflow-core/docs/authoring-and-scheduling/assets.rst
index f7f018c6d97..dbbecd91ef8 100644
--- a/airflow-core/docs/authoring-and-scheduling/assets.rst
+++ b/airflow-core/docs/authoring-and-scheduling/assets.rst
@@ -280,7 +280,7 @@ The following example creates an asset event against the S3 URI ``f"s3://bucket/
 
 .. code-block:: python
 
-    from airflow.sdk.definitions.asset import AssetAlias
+    from airflow.sdk import AssetAlias
 
 
     @task(outlets=[AssetAlias("my-task-outputs")])
@@ -292,7 +292,7 @@ The following example creates an asset event against the S3 URI ``f"s3://bucket/
 
 .. code-block:: python
 
-    from airflow.sdk.definitions.asset.metadata import Metadata
+    from airflow.sdk import Metadata
 
 
     @task(outlets=[AssetAlias("my-task-outputs")])
@@ -304,7 +304,7 @@ Only one asset event is emitted for an added asset, even if it is added to the a
 
 .. code-block:: python
 
-    from airflow.sdk.definitions.asset import AssetAlias
+    from airflow.sdk import AssetAlias
 
 
     @task(


(airflow) 07/25: Added the download button on the assets page (#50045)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 733c7ee97eaab706575b818e3b86c74c1d9f2aac
Author: Aritra Basu <24...@users.noreply.github.com>
AuthorDate: Thu May 1 00:40:41 2025 +0530

    Added the download button on the assets page (#50045)
    
    * Added the download button on the assets
    
    Added for consistency, not sure on if
    people do actually want to download that
    graph
    
    * Update airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx
    
    Co-authored-by: Brent Bovenzi <br...@gmail.com>
    
    * Update AssetGraph.tsx
    
    ---------
    
    Co-authored-by: Brent Bovenzi <br...@gmail.com>
    (cherry picked from commit 77d2fa5b035244944d10914d08e7855a20d573b0)
---
 airflow-core/src/airflow/ui/src/components/Graph/DownloadButton.tsx | 4 ++--
 airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx     | 2 +-
 airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx          | 2 ++
 3 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/Graph/DownloadButton.tsx b/airflow-core/src/airflow/ui/src/components/Graph/DownloadButton.tsx
index fb4cf5f02e8..1b155a65ea9 100644
--- a/airflow-core/src/airflow/ui/src/components/Graph/DownloadButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/Graph/DownloadButton.tsx
@@ -23,7 +23,7 @@ import { FiDownload } from "react-icons/fi";
 
 import { toaster } from "src/components/ui";
 
-export const DownloadButton = ({ dagId }: { readonly dagId: string }) => {
+export const DownloadButton = ({ name }: { readonly name: string }) => {
   const { getNodes, getZoom } = useReactFlow();
 
   const onClick = () => {
@@ -49,7 +49,7 @@ export const DownloadButton = ({ dagId }: { readonly dagId: string }) => {
         .then((dataUrl) => {
           const downloadLink = document.createElement("a");
 
-          downloadLink.setAttribute("download", `${dagId}-graph.png`);
+          downloadLink.setAttribute("download", `${name}-graph.png`);
           downloadLink.setAttribute("href", dataUrl);
           downloadLink.click();
         })
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx
index be47ebd3abf..12744c7d15f 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx
@@ -198,7 +198,7 @@ export const Graph = () => {
         style={{ height: 150, width: 200 }}
         zoomable
       />
-      <DownloadButton dagId={dagId} />
+      <DownloadButton name={dagId} />
     </ReactFlow>
   );
 };
diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx
index 28246ed57c1..42e1c39071f 100644
--- a/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx
@@ -22,6 +22,7 @@ import "@xyflow/react/dist/style.css";
 import { useParams } from "react-router-dom";
 
 import type { AssetResponse } from "openapi/requests/types.gen";
+import { DownloadButton } from "src/components/Graph/DownloadButton";
 import { edgeTypes, nodeTypes } from "src/components/Graph/graphTypes";
 import type { CustomNodeProps } from "src/components/Graph/reactflowUtils";
 import { useGraphLayout } from "src/components/Graph/useGraphLayout";
@@ -84,6 +85,7 @@ export const AssetGraph = ({ asset }: { readonly asset?: AssetResponse }) => {
         pannable
         zoomable
       />
+      <DownloadButton name={asset?.name ?? asset?.uri ?? "asset"} />
     </ReactFlow>
   );
 };


(airflow) 21/25: fix: wrap overflowing texts of asset events (#50173)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 554e15921bac3a6e33e996eef425c4918a091089
Author: Zhen-Lun (Kevin) Hong <ke...@yahoo.com.tw>
AuthorDate: Sun May 4 19:42:39 2025 +0800

    fix: wrap overflowing texts of asset events (#50173)
    
    (cherry picked from commit 0258f0a36610b124137b92f803e8d59fe4182f88)
---
 .../src/airflow/ui/src/components/Assets/AssetEvent.tsx    | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/Assets/AssetEvent.tsx b/airflow-core/src/airflow/ui/src/components/Assets/AssetEvent.tsx
index 1cb0817964e..6d69ae9a1ac 100644
--- a/airflow-core/src/airflow/ui/src/components/Assets/AssetEvent.tsx
+++ b/airflow-core/src/airflow/ui/src/components/Assets/AssetEvent.tsx
@@ -51,7 +51,9 @@ export const AssetEvent = ({
       </Text>
       {Boolean(assetId) ? undefined : (
         <HStack>
-          <FiDatabase />
+          <Box>
+            <FiDatabase />
+          </Box>
           <Tooltip
             content={
               <div>
@@ -62,18 +64,22 @@ export const AssetEvent = ({
             showArrow
           >
             <Link to={`/assets/${event.asset_id}`}>
-              <Text color="fg.info"> {event.name ?? ""} </Text>
+              <Box color="fg.info" overflowWrap="anywhere" padding={0} wordWrap="break-word">
+                {event.name ?? ""}
+              </Box>
             </Link>
           </Tooltip>
         </HStack>
       )}
       <HStack>
-        <Text>Source: </Text>
+        <Box>Source: </Box>
         {source === "" ? (
           <Link
             to={`/dags/${event.source_dag_id}/runs/${event.source_run_id}/tasks/${event.source_task_id}${event.source_map_index > -1 ? `/mapped/${event.source_map_index}` : ""}`}
           >
-            <Text color="fg.info"> {event.source_dag_id} </Text>
+            <Box color="fg.info" overflowWrap="anywhere" padding={0} wordWrap="break-word">
+              {event.source_dag_id}
+            </Box>
           </Link>
         ) : (
           source


(airflow) 16/25: Fix datetime validation (#50116)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit ca8d0114cd15506ac801380a233530a2f8de005c
Author: Guan Ming(Wesley) Chiu <10...@users.noreply.github.com>
AuthorDate: Fri May 2 19:38:31 2025 +0800

    Fix datetime validation (#50116)
    
    (cherry picked from commit 205a672f75159ad1dc10430fde95e28e0b420b23)
---
 .../src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx      | 3 +--
 airflow-core/src/airflow/ui/src/components/DateTimeInput.tsx          | 4 +++-
 .../src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx       | 3 ---
 3 files changed, 4 insertions(+), 6 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
index 668f54a3d4c..3cf45688eed 100644
--- a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
@@ -156,7 +156,7 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
           <Text fontSize="md" fontWeight="semibold" mb={3}>
             Date Range
           </Text>
-          <HStack w="full">
+          <HStack alignItems="flex-start" w="full">
             <Controller
               control={control}
               name="from_date"
@@ -175,7 +175,6 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
                 <Field.Root invalid={Boolean(errors.date) || dataIntervalInvalid} required>
                   <Field.Label>To</Field.Label>
                   <DateTimeInput {...field} max={today} onBlur={resetDateError} size="sm" />
-                  <Field.ErrorText>&nbsp;</Field.ErrorText>
                 </Field.Root>
               )}
             />
diff --git a/airflow-core/src/airflow/ui/src/components/DateTimeInput.tsx b/airflow-core/src/airflow/ui/src/components/DateTimeInput.tsx
index c4fa9010fc2..48c7a41099e 100644
--- a/airflow-core/src/airflow/ui/src/components/DateTimeInput.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DateTimeInput.tsx
@@ -43,7 +43,9 @@ export const DateTimeInput = forwardRef<HTMLInputElement, Props>(({ onChange, va
           target: {
             ...event.target,
             // Return a timezone-aware ISO string
-            value: dayjs(event.target.value).tz(selectedTimezone, true).toISOString(),
+            value: dayjs(event.target.value).isValid()
+              ? dayjs(event.target.value).tz(selectedTimezone, true).toISOString()
+              : "",
           },
         })
       }
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
index ae9e60c812f..6f5562747fe 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
@@ -53,7 +53,6 @@ export const FlexibleForm = ({
         setSectionError(sectionError);
       }
     });
-    console.log("errors1", sectionError);
   }, [flexibleFormDefaultSection, params, sectionError]);
 
   useEffect(() => {
@@ -91,8 +90,6 @@ export const FlexibleForm = ({
     }
   };
 
-  console.log(sectionError);
-
   return Object.entries(params).some(([, param]) => typeof param.schema.section !== "string")
     ? Object.entries(params).map(([, secParam]) => {
         const currentSection = secParam.schema.section ?? flexibleFormDefaultSection;


(airflow) 19/25: Fixes validation error and adds error indicator (#50127)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 5ad8cb83b37d715cf0bf249650561f40fa5717e9
Author: Aritra Basu <24...@users.noreply.github.com>
AuthorDate: Sat May 3 03:11:36 2025 +0530

    Fixes validation error and adds error indicator (#50127)
    
    * Fixes validation error and adds error indicator
    
    Fixes validation failing on first load and
    adds a textual indicator of error
    
    * Update error indicator to icon for trigger dag
    
    (cherry picked from commit 22af7027b69c593c5daebcf195634385e8e8d08d)
---
 .../src/components/FlexibleForm/FlexibleForm.tsx   | 29 +++++++++++++---------
 1 file changed, 17 insertions(+), 12 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
index 6f5562747fe..ff1bd23a198 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
@@ -16,8 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Stack, StackSeparator } from "@chakra-ui/react";
+import { Box, Icon, Stack, StackSeparator, Text } from "@chakra-ui/react";
 import { useCallback, useEffect, useState } from "react";
+import { MdError } from "react-icons/md";
 
 import type { ParamsSpec } from "src/queries/useDagParams";
 import { useParamStore } from "src/queries/useParamStore";
@@ -74,12 +75,14 @@ export const FlexibleForm = ({
     [setParamsDict, setinitialParamDict],
   );
 
-  useEffect(
-    () => () => {
-      recheckSection();
-    },
-    [params, recheckSection],
-  );
+  useEffect(() => {
+    recheckSection();
+    if (sectionError.size === 0) {
+      setError(false);
+    } else {
+      setError(true);
+    }
+  }, [params, setError, recheckSection, sectionError]);
 
   const onUpdate = (_value?: string, error?: unknown) => {
     recheckSection();
@@ -101,11 +104,13 @@ export const FlexibleForm = ({
 
           return (
             <Accordion.Item key={currentSection} value={currentSection}>
-              <Accordion.ItemTrigger
-                color={sectionError.get(currentSection) ? "red" : undefined}
-                cursor="button"
-              >
-                {currentSection}
+              <Accordion.ItemTrigger cursor="button">
+                <Text color={sectionError.get(currentSection) ? "red" : undefined}>{currentSection}</Text>
+                {sectionError.get(currentSection) ? (
+                  <Icon color="red" margin="-1">
+                    <MdError />
+                  </Icon>
+                ) : undefined}
               </Accordion.ItemTrigger>
               <Accordion.ItemContent paddingTop={0}>
                 <Box p={5}>


(airflow) 15/25: Fix duration charts (#50094)

Posted by ka...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit a7e74d33fc4f812aee446a5211a3fe24878207c6
Author: Brent Bovenzi <br...@astronomer.io>
AuthorDate: Thu May 1 18:32:26 2025 -0400

    Fix duration charts (#50094)
    
    (cherry picked from commit c034275bc41606bbeeba3fe123f87ca2c9196642)
---
 airflow-core/src/airflow/ui/src/components/DurationChart.tsx | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx
index 4e525c55e41..6a4e86ff1bd 100644
--- a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx
@@ -34,7 +34,7 @@ import { Bar } from "react-chartjs-2";
 
 import type { TaskInstanceResponse, DAGRunResponse } from "openapi/requests/types.gen";
 import { system } from "src/theme";
-import { pluralize, getDuration } from "src/utils";
+import { pluralize } from "src/utils";
 
 ChartJS.register(
   CategoryScale,
@@ -55,6 +55,8 @@ const average = (ctx: PartialEventContext, index: number) => {
 
 type RunResponse = DAGRunResponse | TaskInstanceResponse;
 
+const getDuration = (start: string, end: string | null) => dayjs.duration(dayjs(end).diff(start)).asSeconds();
+
 export const DurationChart = ({
   entries,
   kind,