diff --git a/airflow/example_dags/example_params_ui_tutorial.py b/airflow/example_dags/example_params_ui_tutorial.py index 3d2ec97c9976b..c07f9fc7c2bd9 100644 --- a/airflow/example_dags/example_params_ui_tutorial.py +++ b/airflow/example_dags/example_params_ui_tutorial.py @@ -55,7 +55,7 @@ "most_loved_number": Param( 42, type="integer", - title="You favorite number", + title="Your favorite number", description_html="""Everybody should have a favorite number. Not only math teachers. If you can not think of any at the moment please think of the 42 which is very famous because of the book @@ -71,6 +71,62 @@ description="You can use JSON schema enum's to generate drop down selection boxes.", enum=[f"value {i}" for i in range(16, 64)], ), + # You can also label the selected values via values_display attribute + "pick_with_label": Param( + 3, + type="number", + title="Select one Number", + description="With drop down selections you can also have nice display labels for the values.", + enum=[*range(1, 10)], + values_display={ + 1: "One", + 2: "Two", + 3: "Three", + 4: "Four - is like you take three and get one for free!", + 5: "Five", + 6: "Six", + 7: "Seven", + 8: "Eight", + 9: "Nine", + }, + ), + # If you want to have a list box with proposals but not enforcing a fixed list + # then you can use the examples feature of JSON schema + "proposals": Param( + "some value", + type="string", + title="Field with proposals", + description="You can use JSON schema examples's to generate drop down selection boxes " + "but allow also to enter custom values. Try typing an 'a' and see options.", + examples=( + "Alpha,Bravo,Charlie,Delta,Echo,Foxtrot,Golf,Hotel,India,Juliett,Kilo,Lima,Mike,November,Oscar,Papa," + "Quebec,Romeo,Sierra,Tango,Uniform,Victor,Whiskey,X-ray,Yankee,Zulu" + ).split(","), + ), + # If you want to select multiple items from a fixed list JSON schema des not allow to use enum + # In this case the type "array" is being used together with "examples" as pick list + "multi_select": Param( + ["two", "three"], + "Select from the list of options.", + type="array", + title="Multi Select", + examples=["one", "two", "three", "four", "five"], + ), + # A multiple options selection can also be combined with values_display + "multi_select_with_label": Param( + ["2", "3"], + "Select from the list of options. See that options can have nicer text and still technical values" + "are propagated as values during trigger to the DAG.", + type="array", + title="Multi Select with Labels", + examples=["1", "2", "3", "4", "5"], + values_display={ + "1": "One box of choccolate", + "2": "Two bananas", + "3": "Three apples", + # Note: Value display mapping does not need to be complete.s + }, + ), # Boolean as proper parameter with description "bool": Param( True, diff --git a/airflow/www/static/js/trigger.js b/airflow/www/static/js/trigger.js index 27fcc099726cf..6ada3d615eaed 100644 --- a/airflow/www/static/js/trigger.js +++ b/airflow/www/static/js/trigger.js @@ -17,7 +17,7 @@ * under the License. */ -/* global document, CodeMirror, window */ +/* global document, CodeMirror, window, $ */ let jsonForm; const objectFields = new Map(); @@ -46,7 +46,19 @@ function updateJSONconf() { values[values.length] = lines[j].trim(); } } - params[keyName] = values.length === 0 ? params[keyName] : values; + params[keyName] = values.length === 0 ? null : values; + } else if ( + elements[i].attributes.valuetype && + elements[i].attributes.valuetype.value === "multiselect" + ) { + const { options } = elements[i]; + const values = []; + for (let j = 0; j < options.length; j += 1) { + if (options[j].selected) { + values[values.length] = options[j].value; + } + } + params[keyName] = values.length === 0 ? null : values; } else if (elements[i].value.length === 0) { params[keyName] = null; } else if ( @@ -104,7 +116,7 @@ function initForm() { jsonForm.setSize(null, height); if (formHasFields) { - // Apply JSON formatting and linting to all object fields in the form + // Initialize jQuery and Chakra fields const elements = document.getElementById("trigger_form"); for (let i = 0; i < elements.length; i += 1) { if (elements[i].name && elements[i].name.startsWith("element_")) { @@ -112,6 +124,7 @@ function initForm() { elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === "object" ) { + // Apply JSON formatting and linting to all object fields in the form const field = CodeMirror.fromTextArea(elements[i], { lineNumbers: true, mode: { name: "javascript", json: true }, @@ -120,6 +133,14 @@ function initForm() { }); field.on("blur", updateJSONconf); objectFields.set(elements[i].name, field); + } else if (elements[i].nodeName === "SELECT") { + // Activate select2 multi select boxes + const elementId = `#${elements[i].name}`; + $(elementId).select2({ + placeholder: "Select Values", + allowClear: true, + }); + elements[i].addEventListener("blur", updateJSONconf); } else if (elements[i].type === "checkbox") { elements[i].addEventListener("change", updateJSONconf); } else { @@ -174,7 +195,9 @@ function initForm() { setTimeout(updateJSONconf, 100); } } -initForm(); +$(document).ready(() => { + initForm(); +}); window.updateJSONconf = updateJSONconf; @@ -212,6 +235,8 @@ function setRecentConfig(e) { objectFields .get(`element_${keys[i]}`) .setValue(JSON.stringify(newValue, null, 4)); + } else if (element.nodeName === "SELECT") { + $(`#${element.name}`).select2("val", [newValue]); } else { element.value = newValue; } diff --git a/airflow/www/templates/airflow/trigger.html b/airflow/www/templates/airflow/trigger.html index a7969f7cbb9da..56ad656883660 100644 --- a/airflow/www/templates/airflow/trigger.html +++ b/airflow/www/templates/airflow/trigger.html @@ -72,12 +72,35 @@ {% elif "enum" in form_details.schema and form_details.schema.enum %} {% elif form_details.schema and "array" in form_details.schema.type %} + {% if "examples" in form_details.schema and form_details.schema.examples %} + + {% else %} + {% endif %} {% elif form_details.schema and "object" in form_details.schema.type %} {% elif form_details.schema and ("integer" in form_details.schema.type or "number" in form_details.schema.type) %} - {% else %} - + {% if "examples" in form_details.schema and form_details.schema.examples %} + + {% for option in form_details.schema.examples -%} + + {% endfor -%} + + {% endif %} {% endif %} {% if form_details.description -%} {{ form_details.description }} diff --git a/docs/apache-airflow/core-concepts/params.rst b/docs/apache-airflow/core-concepts/params.rst index eacaef3c5d2fd..7619d56171d46 100644 --- a/docs/apache-airflow/core-concepts/params.rst +++ b/docs/apache-airflow/core-concepts/params.rst @@ -189,13 +189,19 @@ The following features are supported in the Trigger UI Form: You can add the parameters ``minimum`` and ``maximum`` to restrict number range accepted. - ``boolean``: Generates a toggle button to be used as ``True`` or ``False``. - ``date``, ``datetime`` and ``time``: Generate date and/or time picker - - ``array``: Generates a HTML multi line text field, every line edited will be made into a string array as value + - ``array``: Generates a HTML multi line text field, every line edited will be made into a string array as value. + if you add the attribute ``example`` with a list, a multi-value select option will be generated. - ``object``: Generates a JSON entry field - Note: Per default if you specify a type, a field will be made required with input - because of JSON validation. If you want to have a field value being added optional only, you must allow JSON schema validation allowing null values via: ``type=["null", "string"]`` -- The Param attribute ``enum`` generates a drop-down select list. As of JSON validation, a value must be selected. +- The Param attribute ``enum`` generates a drop-down select list for scalar values. As of JSON validation, a value must be selected or + the field must be marked as optional explicit. +- If you want to present proposals for scalar values (not restricting the user to a fixed ``enum`` as above) you can make use of + ``examples`` which is a list of items. +- For select drop-downs generated via ``enum`` or multi-value selects you can add the attribute ``values_display`` with a dict and + map data values to display labels. - If a form field is left empty, it is passed as ``None`` value to the params dict. - Form fields are rendered in the order of definition. - If you want to add sections to the Form, add the parameter ``section`` to each field. The text will be used as section label.