From 1c463bdc7d3aadd0bf71cd5ac5698ee421353088 Mon Sep 17 00:00:00 2001 From: SPCX Date: Sun, 15 Feb 2026 21:26:01 +0100 Subject: [PATCH 1/2] Add section_collapsed property to control default accordion state in DAG trigger form Allow DAG authors to define which param sections start collapsed in the trigger form by setting section_collapsed=True on a Param. Sections without this flag (or with section_collapsed=False) start expanded. When no section uses the flag, the existing default behavior is preserved. Co-Authored-By: Claude Opus 4.6 --- .../example_params_ui_tutorial.py | 1 + .../airflow/ui/src/components/ConfigForm.tsx | 31 ++++++++++++++++++- .../airflow/ui/src/queries/useDagParams.ts | 1 + .../airflow/ui/src/queries/useParamStore.ts | 1 + 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/example_dags/example_params_ui_tutorial.py b/airflow-core/src/airflow/example_dags/example_params_ui_tutorial.py index 40e4667d35b6f..062b5e3ca6cf2 100644 --- a/airflow-core/src/airflow/example_dags/example_params_ui_tutorial.py +++ b/airflow-core/src/airflow/example_dags/example_params_ui_tutorial.py @@ -202,6 +202,7 @@ title="Array of numbers", items={"type": "number"}, section="Special advanced stuff with form fields", + section_collapsed=True, ), "multiline_text": Param( "A multiline text Param\nthat will keep the newline\ncharacters in its value.", diff --git a/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx index 65adf1bfca302..d299bacab6443 100644 --- a/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx +++ b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx @@ -17,6 +17,7 @@ * under the License. */ import { Box, Field } from "@chakra-ui/react"; +import { useMemo } from "react"; import { type Control, type FieldValues, type Path, Controller } from "react-hook-form"; import { useTranslation } from "react-i18next"; @@ -55,6 +56,33 @@ const ConfigForm = ({ const { t: translate } = useTranslation(["components", "common"]); const { conf, setConf } = useParamStore(); + const defaultExpandedSections = useMemo(() => { + const paramsDict = initialParamsDict.paramsDict; + const sectionCollapsed = new Map(); + let hasAnyConfig = false; + + for (const param of Object.values(paramsDict)) { + const section = param.schema.section ?? flexibleFormDefaultSection; + + if (!sectionCollapsed.has(section)) { + const collapsed = param.schema.section_collapsed; + + if (collapsed !== undefined) { + hasAnyConfig = true; + } + sectionCollapsed.set(section, collapsed === true); + } + } + + if (!hasAnyConfig) { + return [flexibleFormDefaultSection]; + } + + return [...sectionCollapsed.entries()] + .filter(([, collapsed]) => !collapsed) + .map(([section]) => section); + }, [initialParamsDict.paramsDict]); + const validateAndPrettifyJson = (value: string) => { try { const parsedJson = JSON.parse(value) as JSON; @@ -83,8 +111,9 @@ const ConfigForm = ({ return ( | string | undefined; values_display: Record | undefined; diff --git a/airflow-core/src/airflow/ui/src/queries/useParamStore.ts b/airflow-core/src/airflow/ui/src/queries/useParamStore.ts index 484197f92e171..aea87846e78da 100644 --- a/airflow-core/src/airflow/ui/src/queries/useParamStore.ts +++ b/airflow-core/src/airflow/ui/src/queries/useParamStore.ts @@ -35,6 +35,7 @@ export const paramPlaceholder: ParamSpec = { minimum: undefined, minLength: undefined, section: undefined, + section_collapsed: undefined, title: undefined, type: undefined, values_display: undefined, From 99b34794b9b9352f69a1209e4ff8d58e6555e09d Mon Sep 17 00:00:00 2001 From: SPCX Date: Sun, 15 Feb 2026 21:45:34 +0100 Subject: [PATCH 2/2] Add example DAG demonstrating section_collapsed feature Four sections with different collapsed states: - "Basic settings" and "Notification settings" start expanded - "Advanced options" and "Debug" start collapsed (section_collapsed=True) Co-Authored-By: Claude Opus 4.6 --- .../example_params_sections_collapsed.py | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 airflow-core/src/airflow/example_dags/example_params_sections_collapsed.py diff --git a/airflow-core/src/airflow/example_dags/example_params_sections_collapsed.py b/airflow-core/src/airflow/example_dags/example_params_sections_collapsed.py new file mode 100644 index 0000000000000..bbaad1b01c3ff --- /dev/null +++ b/airflow-core/src/airflow/example_dags/example_params_sections_collapsed.py @@ -0,0 +1,144 @@ +# +# 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. +"""Example DAG demonstrating the section_collapsed parameter for trigger form sections. + +This DAG shows how to use the ``section_collapsed`` attribute on ``Param`` to control +which sections of the trigger form start expanded and which start collapsed. +The first ``Param`` in each section that sets ``section_collapsed=True`` will cause +the entire section to be rendered collapsed by default. Sections without this flag +start expanded. Users can still manually expand or collapse any section. +""" + +from __future__ import annotations + +import datetime +import json +from pathlib import Path + +from airflow.sdk import DAG, Param, task + +with DAG( + dag_id=Path(__file__).stem, + dag_display_name="Params Sections Collapsed", + description=__doc__.partition(".")[0], + doc_md=__doc__, + schedule=None, + start_date=datetime.datetime(2022, 3, 4), + catchup=False, + tags=["example", "params", "ui"], + params={ + # --- Section: Basic settings (expanded by default) --- + "environment": Param( + "production", + type="string", + title="Target environment", + description="Select the environment to deploy to.", + enum=["development", "staging", "production"], + section="Basic settings", + ), + "dry_run": Param( + False, + type="boolean", + title="Dry run", + description="If enabled, no actual changes will be made.", + section="Basic settings", + ), + # --- Section: Notification settings (expanded by default) --- + "notify_on_success": Param( + True, + type="boolean", + title="Notify on success", + description="Send a notification when the DAG run succeeds.", + section="Notification settings", + ), + "notify_on_failure": Param( + True, + type="boolean", + title="Notify on failure", + description="Send a notification when the DAG run fails.", + section="Notification settings", + ), + "notification_email": Param( + "team@example.com", + type="string", + title="Notification email", + description="Email address for notifications.", + section="Notification settings", + ), + # --- Section: Advanced options (collapsed by default) --- + "max_retries": Param( + 3, + type="integer", + title="Max retries", + description="Maximum number of retries for failed tasks.", + minimum=0, + maximum=10, + section="Advanced options", + section_collapsed=True, + ), + "retry_delay_seconds": Param( + 60, + type="integer", + title="Retry delay (seconds)", + description="Delay between retries in seconds.", + minimum=10, + maximum=600, + section="Advanced options", + ), + "timeout_minutes": Param( + 30, + type="integer", + title="Timeout (minutes)", + description="Overall timeout for the DAG run in minutes.", + minimum=1, + maximum=120, + section="Advanced options", + ), + # --- Section: Debug (collapsed by default) --- + "verbose_logging": Param( + False, + type="boolean", + title="Verbose logging", + description="Enable verbose logging for debugging purposes.", + section="Debug", + section_collapsed=True, + ), + "log_level": Param( + "INFO", + type="string", + title="Log level", + description="Set the log level.", + enum=["DEBUG", "INFO", "WARNING", "ERROR"], + section="Debug", + ), + "dump_config": Param( + False, + type="boolean", + title="Dump config", + description="Print the full configuration to the logs before running.", + section="Debug", + ), + }, +) as dag: + + @task(task_display_name="Show configuration") + def show_config(**kwargs) -> None: + params = kwargs["params"] + print(f"DAG triggered with configuration:\n\n{json.dumps(params, indent=4)}\n") + + show_config()