You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by GitBox <gi...@apache.org> on 2022/10/14 18:33:04 UTC

[GitHub] [airflow] jens-scheffler-bosch opened a new pull request, #27063: [WIP/Preview] Feature/26215 Proposal for AIP-50 Trigger UI based on FAB

jens-scheffler-bosch opened a new pull request, #27063:
URL: https://github.com/apache/airflow/pull/27063

   This PR is a (draft/PoC) implementation for a trigger UI based on ideas and discussion from [AIP-50 Trigger DAG UI Extension with Flexible User Form Concept](https://cwiki.apache.org/confluence/display/AIRFLOW/AIP-50+Trigger+DAG+UI+Extension+with+Flexible+User+Form+Concept) based on current FAB UI infrastructure.
   
   Due to the feedback received on [AIP-50](https://cwiki.apache.org/confluence/display/AIRFLOW/AIP-50+Trigger+DAG+UI+Extension+with+Flexible+User+Form+Concept) the PR is based on the [`Params` toolings](https://airflow.apache.org/docs/apache-airflow/stable/concepts/params.html) already available and leverages with minimal changes 80% of desired scope.
   
   Coverage/Scope of this PR:
   - AIP-50 Part 1+3 (fully covered): Specifying Trigger UI on DAG Level + Standard Implementation for Forms
       - By defining `params` or usage of existing `params` dict a form as UI is generated dynamically
       - Existing JSON schema is used as good as possible with slight extensions for purpose of form rendering
       - Compared to the initial idea to have a "new" form as dialog, the proposed PR now extends the existing JSON entry UI by:
           - If `params` are defined on a DAG a form is generated
           - If no `params` are defined the previous JSON based entry is presented
           - Why? Benefits? Part 2 was not implemented and showing a form if no parameters are defined is not good (=empty form) and the current proposal also allows modification of JSON if needed. Two dialogs might also confuse users (?) where in this proposal one provides all functionality needed. Also the extension here in this PR is very thin, therefore no additional UI is needed in our opinion.
       - Small derivations compared to current AIP-50:
           - Field type and rendering of field is not only made depending on Schema but also auto-discovered for common data types (number, list, json, boolean) which adds an easy entry for existing DAGs w/o needs of change == for free.
           - Field type `hidden` was implemented by using the JSON schema attribute `constant` which assumes that only one constant value is permitted, no additional flag needed.
           - Advanced section of a entry form was generalized allowing to define any/multiple sections of the form (not limited to one "Advanced")
   - AIP-50 Part 2 (NOT covered): UI Changes for Trigger Button
       - During implementation we saw that either performance would drain or DB schema would need to be extended besides anticipated code changes
       - As assumption is that part 2 can be separated and have a follow-up discussion for solution space we decided to take this out for the preview
       - As also no additional form was implemented, so no changes in the UI for triggering needed for the moment.
   - AIP-50 Part 4 (fully covered): Examples:
       - Two examples added within this PR
           - One simple example as "showcase"
           - One "Tutorial" with a lot of examples for large coverage and template for users to be able to copy
   - AIP-50 Part 5 (NOT covered): Documentation
       - Skipped this in the WIP state, if review is positive of course will be added. Until then the tutorial example DAG is the best way to understand how it works.
       - Proposal is to extend the existing page [Params](https://airflow.apache.org/docs/apache-airflow/stable/concepts/params.html) to add details about UI, options and supported parameters etc.
   
   Note that I skipped adding unit tests, these will be added (together with documentation) before merge if positive feedback is showing a way forward.
   
   Please review and provide feedback :-D
   
   Questions that I am explicitly interested in during the review:
   - What would be additional merge pre-requisites to move ahead?
   - (I am an positive thinker) Could this PR already be added early into a next 2.4.x release or will it need to get into a 2.5.0 release? Any condition to bring this to the field early?
   - Is it accepted to add this (small==680 LOC) change to FAB ui even knowing that this will be replaced by AIP-38 in "soon" time? Structure-wise I assume the implementation except the UI can be taken over w/o a breaking change.
   - Discussion about AIP-50 Part 2) The gap which has been discovered during the implementation of this PR is that the DAG overview page is rendered by a DB query. To bring the part 2) into this we we either (1) need to extend the DB scheme so that the property of the run options can be fetched from DB quick, or (2) we would need to load the `DagBag` of each row to discover options for UI trigger (which would be N=25 additional queries to DB) which slows down responsiveness or (3) we load the details of the Run buttons async which adds complexity for a convenience option - or (4) we just leave it as it is. (4) also means that this PR would complete the AIP-50.
   - I have noticed that if somebody use the trigger Dag Run button without parameters form then always an empty Dict is passed as `DagRun.conf`, which means a DAG must always be prepared to accept parameters but also might be called without. Also this conflicts to the idea that parameters are schema validated if they can still be totally missing. Would it make sense to (1) change the default behavior so that the default trigger (w/o form or scheduled) is changed such that the parameters are supplied always per default? Would ease DAG logic not needing to handle all situations when parameters are missing. Otherwise (2) it might be a good configuration option per DAG to define if parameters shall be taken as `DagRun.conf` if none are given. See also discussion in [Airflow Slack](https://apache-airflow.slack.com/archives/CCQ7EGB1P/p1665651809736739).
   
   Open Items before any merge/leaving WIP state:
   [  ]  Positive response from reviewer(s)
   [  ]  Pytests added
   [  ]  Documentation added
   
   related: #26215
   closes: #11054 
   
   FYI @clellmann @wolfdn


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] eladkal commented on pull request #27063: Feature/26215 AIP-50 Trigger UI based on FAB

Posted by GitBox <gi...@apache.org>.
eladkal commented on PR #27063:
URL: https://github.com/apache/airflow/pull/27063#issuecomment-1363851087

   > Now time for a final review, then (in my view) ready to be merged.
   
   I don't see vote thread on the mailing list


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] jens-scheffler-bosch commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "jens-scheffler-bosch (via GitHub)" <gi...@apache.org>.
jens-scheffler-bosch commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1088323671


##########
airflow/www/static/js/trigger.js:
##########
@@ -19,33 +19,190 @@
 
 /* global document, CodeMirror, window */
 
-const textArea = document.getElementById('json');
+let jsonForm;
+const objectFields = new Map();
 const recentConfigList = document.getElementById('recent_configs');
-const minHeight = 300;
-const maxHeight = window.innerHeight - 450;
-const height = maxHeight > minHeight ? maxHeight : minHeight;
-
-CodeMirror.fromTextArea(textArea, {
-  lineNumbers: true,
-  mode: {
-    name: 'javascript',
-    json: true,
-  },
-  gutters: ['CodeMirror-lint-markers'],
-  lint: true,
-})
-  .setSize(null, height);
+
+/**
+ * Update the generated JSON DagRun.conf JSON field if any field changed
+ */
+function updateJSONconf() {
+  const jsonStart = document.getElementById('json_start').value;
+  const params = JSON.parse(jsonStart);
+  const elements = document.getElementById('trigger_form');
+  for (let i = 0; i < elements.length; i += 1) {
+    if (elements[i].name && elements[i].name.startsWith('element_')) {
+      const keyName = elements[i].name.substr(8);
+      if (elements[i].type === 'checkbox') {
+        params[keyName] = elements[i].checked;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'array') {
+        const lines = elements[i].value.split('\n');
+        const values = [];
+        for (let j = 0; j < lines.length; j += 1) {
+          if (lines[j].trim().length > 0) {
+            values[values.length] = lines[j].trim();
+          }
+        }
+        params[keyName] = values;
+      } else if (elements[i].value.length === 0) {
+        params[keyName] = null;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'object') {
+        try {
+          const textValue = objectFields.get(elements[i].name).getValue();
+          if (textValue.length > 0) {
+            const objValue = JSON.parse(textValue);
+            params[keyName] = objValue;
+            objectFields.get(elements[i].name).setValue(JSON.stringify(objValue, null, 4));
+          } else {
+            params[keyName] = null;
+          }
+        } catch (e) {
+          // ignore JSON parsing errors

Review Comment:
   JSON is validated before form submission, I removed any alerting here not to bother user during entry. Added a comment so that future reader knows.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] jens-scheffler-bosch commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "jens-scheffler-bosch (via GitHub)" <gi...@apache.org>.
jens-scheffler-bosch commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1088336825


##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -27,11 +27,93 @@
   <link rel="stylesheet" type="text/css" href="{{ url_for_asset('switch.css') }}">
 {% endblock %}
 
+{% macro form_element(form_key, form_details) %}
+<tr>
+  <td class="col-lg-2">
+    <label for="element_{{ form_key }}" control-label="">
+      {%- if "title" in form_details.schema and form_details.schema.title -%}
+        {{ form_details.schema.title }}
+      {%- else -%}
+        {{ form_key }}
+      {%- endif -%}
+      {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type and not "boolean" in form_details.schema.type -%}
+        <strong style="color: red"> *</strong>
+      {%- endif -%}
+    : </label>
+  </td>
+  <td>
+    {% if "custom_html_form" in form_details.schema %}
+      {{ form_details.schema.custom_html_form | replace("{name}", "element_" + form_key) | replace("{value}", form_details.value) }}
+    {% elif "type" in form_details.schema and form_details.schema.type == "boolean" %}
+      <label for="element_{{ form_key }}" control-label="">
+        <input class="switch-input" name="element_{{ form_key }}" id="element_{{ form_key }}" type="checkbox"
+          {%- if form_details.value %} checked="checked"{%- endif -%}/>
+        <span class="switch" aria-hidden="true"></span>
+      </label>

Review Comment:
   These lines render a boolean switch.
   ![image](https://user-images.githubusercontent.com/95105677/214949669-fc25cca3-b3e5-44f5-9932-61902d685222.png)
   



##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -27,11 +27,93 @@
   <link rel="stylesheet" type="text/css" href="{{ url_for_asset('switch.css') }}">
 {% endblock %}
 
+{% macro form_element(form_key, form_details) %}
+<tr>
+  <td class="col-lg-2">
+    <label for="element_{{ form_key }}" control-label="">
+      {%- if "title" in form_details.schema and form_details.schema.title -%}
+        {{ form_details.schema.title }}
+      {%- else -%}
+        {{ form_key }}
+      {%- endif -%}
+      {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type and not "boolean" in form_details.schema.type -%}
+        <strong style="color: red"> *</strong>
+      {%- endif -%}
+    : </label>
+  </td>
+  <td>
+    {% if "custom_html_form" in form_details.schema %}
+      {{ form_details.schema.custom_html_form | replace("{name}", "element_" + form_key) | replace("{value}", form_details.value) }}
+    {% elif "type" in form_details.schema and form_details.schema.type == "boolean" %}
+      <label for="element_{{ form_key }}" control-label="">
+        <input class="switch-input" name="element_{{ form_key }}" id="element_{{ form_key }}" type="checkbox"
+          {%- if form_details.value %} checked="checked"{%- endif -%}/>
+        <span class="switch" aria-hidden="true"></span>
+      </label>
+    {% elif "format" in form_details.schema and "date-time" in form_details.schema.format %}
+      <div class="input-group datetime datetimepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>

Review Comment:
   These lines render a date-time picker (Unfortunately I'm not able to screen-capture the pupp of the date/time picker but it is the same like on other UI parts:
   ![image](https://user-images.githubusercontent.com/95105677/214950315-971300b7-9448-4630-a83d-7a359ae7346b.png)
   



##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -27,11 +27,93 @@
   <link rel="stylesheet" type="text/css" href="{{ url_for_asset('switch.css') }}">
 {% endblock %}
 
+{% macro form_element(form_key, form_details) %}
+<tr>
+  <td class="col-lg-2">
+    <label for="element_{{ form_key }}" control-label="">
+      {%- if "title" in form_details.schema and form_details.schema.title -%}
+        {{ form_details.schema.title }}
+      {%- else -%}
+        {{ form_key }}
+      {%- endif -%}
+      {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type and not "boolean" in form_details.schema.type -%}
+        <strong style="color: red"> *</strong>
+      {%- endif -%}
+    : </label>
+  </td>

Review Comment:
   This block renders the form label, either using the key or the display label if given. A red star is added if field is required to have content before submit:
   ![image](https://user-images.githubusercontent.com/95105677/214949400-ef4b614d-7c59-4e02-90d2-0fce08f801df.png)
   



##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -27,11 +27,93 @@
   <link rel="stylesheet" type="text/css" href="{{ url_for_asset('switch.css') }}">
 {% endblock %}
 
+{% macro form_element(form_key, form_details) %}
+<tr>
+  <td class="col-lg-2">
+    <label for="element_{{ form_key }}" control-label="">
+      {%- if "title" in form_details.schema and form_details.schema.title -%}
+        {{ form_details.schema.title }}
+      {%- else -%}
+        {{ form_key }}
+      {%- endif -%}
+      {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type and not "boolean" in form_details.schema.type -%}
+        <strong style="color: red"> *</strong>
+      {%- endif -%}
+    : </label>
+  </td>
+  <td>
+    {% if "custom_html_form" in form_details.schema %}
+      {{ form_details.schema.custom_html_form | replace("{name}", "element_" + form_key) | replace("{value}", form_details.value) }}
+    {% elif "type" in form_details.schema and form_details.schema.type == "boolean" %}
+      <label for="element_{{ form_key }}" control-label="">
+        <input class="switch-input" name="element_{{ form_key }}" id="element_{{ form_key }}" type="checkbox"
+          {%- if form_details.value %} checked="checked"{%- endif -%}/>
+        <span class="switch" aria-hidden="true"></span>
+      </label>
+    {% elif "format" in form_details.schema and "date-time" in form_details.schema.format %}
+      <div class="input-group datetime datetimepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "date" in form_details.schema.format %}
+      <div class="input-group datetime datepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "time" in form_details.schema.format %}
+      <div class="input-group datetime timepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %}
+          value="{% if form_details.value %}{{ form_details.value[0:2] }}00-01-01 {{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "enum" in form_details.schema and form_details.schema.enum %}
+      <select class="my_select2 form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" data-placeholder="Select Value"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}" onchange="updateJSONconf();"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif %}>
+        {% for option in form_details.schema.enum -%}
+        <option>{{ option }}</option>
+        {% endfor -%}
+      </select>
+    {% elif form_details.schema and "array" in form_details.schema.type %}
+      <textarea class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" valuetype="array" rows="6"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif -%}>
+        {%- for txt in form_details.value -%}
+          {{ txt }}{{ "\n" }}
+        {%- endfor -%}
+      </textarea>

Review Comment:
   These lines generate a text list which will be used for array of strings:
   ![image](https://user-images.githubusercontent.com/95105677/214950963-8a381d7c-24b0-4696-992c-f4cf4104db90.png)
   



##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -27,11 +27,93 @@
   <link rel="stylesheet" type="text/css" href="{{ url_for_asset('switch.css') }}">
 {% endblock %}
 
+{% macro form_element(form_key, form_details) %}
+<tr>
+  <td class="col-lg-2">
+    <label for="element_{{ form_key }}" control-label="">
+      {%- if "title" in form_details.schema and form_details.schema.title -%}
+        {{ form_details.schema.title }}
+      {%- else -%}
+        {{ form_key }}
+      {%- endif -%}
+      {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type and not "boolean" in form_details.schema.type -%}
+        <strong style="color: red"> *</strong>
+      {%- endif -%}
+    : </label>
+  </td>
+  <td>
+    {% if "custom_html_form" in form_details.schema %}
+      {{ form_details.schema.custom_html_form | replace("{name}", "element_" + form_key) | replace("{value}", form_details.value) }}
+    {% elif "type" in form_details.schema and form_details.schema.type == "boolean" %}
+      <label for="element_{{ form_key }}" control-label="">
+        <input class="switch-input" name="element_{{ form_key }}" id="element_{{ form_key }}" type="checkbox"
+          {%- if form_details.value %} checked="checked"{%- endif -%}/>
+        <span class="switch" aria-hidden="true"></span>
+      </label>
+    {% elif "format" in form_details.schema and "date-time" in form_details.schema.format %}
+      <div class="input-group datetime datetimepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "date" in form_details.schema.format %}
+      <div class="input-group datetime datepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "time" in form_details.schema.format %}
+      <div class="input-group datetime timepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %}
+          value="{% if form_details.value %}{{ form_details.value[0:2] }}00-01-01 {{ form_details.value }}{% endif %}" />
+      </div>

Review Comment:
   Time picker (w/o date):
   ![image](https://user-images.githubusercontent.com/95105677/214950524-cddc0f65-0855-4104-bf52-1015df3f6dd6.png)
   



##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -27,11 +27,93 @@
   <link rel="stylesheet" type="text/css" href="{{ url_for_asset('switch.css') }}">
 {% endblock %}
 
+{% macro form_element(form_key, form_details) %}
+<tr>
+  <td class="col-lg-2">
+    <label for="element_{{ form_key }}" control-label="">
+      {%- if "title" in form_details.schema and form_details.schema.title -%}
+        {{ form_details.schema.title }}
+      {%- else -%}
+        {{ form_key }}
+      {%- endif -%}
+      {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type and not "boolean" in form_details.schema.type -%}
+        <strong style="color: red"> *</strong>
+      {%- endif -%}
+    : </label>
+  </td>
+  <td>
+    {% if "custom_html_form" in form_details.schema %}
+      {{ form_details.schema.custom_html_form | replace("{name}", "element_" + form_key) | replace("{value}", form_details.value) }}
+    {% elif "type" in form_details.schema and form_details.schema.type == "boolean" %}
+      <label for="element_{{ form_key }}" control-label="">
+        <input class="switch-input" name="element_{{ form_key }}" id="element_{{ form_key }}" type="checkbox"
+          {%- if form_details.value %} checked="checked"{%- endif -%}/>
+        <span class="switch" aria-hidden="true"></span>
+      </label>
+    {% elif "format" in form_details.schema and "date-time" in form_details.schema.format %}
+      <div class="input-group datetime datetimepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "date" in form_details.schema.format %}
+      <div class="input-group datetime datepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "time" in form_details.schema.format %}
+      <div class="input-group datetime timepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %}
+          value="{% if form_details.value %}{{ form_details.value[0:2] }}00-01-01 {{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "enum" in form_details.schema and form_details.schema.enum %}
+      <select class="my_select2 form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" data-placeholder="Select Value"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}" onchange="updateJSONconf();"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif %}>
+        {% for option in form_details.schema.enum -%}
+        <option>{{ option }}</option>
+        {% endfor -%}
+      </select>

Review Comment:
   These lines generate a drop-down select:
   Shot 1 - view:
   ![image](https://user-images.githubusercontent.com/95105677/214950812-cd440798-62a2-43c3-a30f-33c10762f601.png)
   
   Shot 2 - dowp down open incl. filter - same linke on other select boxes in UI:
   ![image](https://user-images.githubusercontent.com/95105677/214950761-94d22897-09f0-48f0-a1f2-7af7f4fb7d24.png)
   



##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -27,11 +27,93 @@
   <link rel="stylesheet" type="text/css" href="{{ url_for_asset('switch.css') }}">
 {% endblock %}
 
+{% macro form_element(form_key, form_details) %}
+<tr>
+  <td class="col-lg-2">
+    <label for="element_{{ form_key }}" control-label="">
+      {%- if "title" in form_details.schema and form_details.schema.title -%}
+        {{ form_details.schema.title }}
+      {%- else -%}
+        {{ form_key }}
+      {%- endif -%}
+      {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type and not "boolean" in form_details.schema.type -%}
+        <strong style="color: red"> *</strong>
+      {%- endif -%}
+    : </label>
+  </td>
+  <td>
+    {% if "custom_html_form" in form_details.schema %}
+      {{ form_details.schema.custom_html_form | replace("{name}", "element_" + form_key) | replace("{value}", form_details.value) }}
+    {% elif "type" in form_details.schema and form_details.schema.type == "boolean" %}
+      <label for="element_{{ form_key }}" control-label="">
+        <input class="switch-input" name="element_{{ form_key }}" id="element_{{ form_key }}" type="checkbox"
+          {%- if form_details.value %} checked="checked"{%- endif -%}/>
+        <span class="switch" aria-hidden="true"></span>
+      </label>
+    {% elif "format" in form_details.schema and "date-time" in form_details.schema.format %}
+      <div class="input-group datetime datetimepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "date" in form_details.schema.format %}
+      <div class="input-group datetime datepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "time" in form_details.schema.format %}
+      <div class="input-group datetime timepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %}
+          value="{% if form_details.value %}{{ form_details.value[0:2] }}00-01-01 {{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "enum" in form_details.schema and form_details.schema.enum %}
+      <select class="my_select2 form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" data-placeholder="Select Value"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}" onchange="updateJSONconf();"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif %}>
+        {% for option in form_details.schema.enum -%}
+        <option>{{ option }}</option>
+        {% endfor -%}
+      </select>
+    {% elif form_details.schema and "array" in form_details.schema.type %}
+      <textarea class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" valuetype="array" rows="6"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif -%}>
+        {%- for txt in form_details.value -%}
+          {{ txt }}{{ "\n" }}
+        {%- endfor -%}
+      </textarea>
+    {% elif form_details.schema and "object" in form_details.schema.type %}
+      <textarea class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" valuetype="object" rows="6"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif -%}>
+        {{- form_details.value | tojson() -}}
+      </textarea>

Review Comment:
   If some data is an object/dict then we render a text box here which is converted to a JSON entry box during page open:
   ![image](https://user-images.githubusercontent.com/95105677/214951102-1abcfe98-e74c-4816-91aa-b14445666466.png)
   



##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -27,11 +27,93 @@
   <link rel="stylesheet" type="text/css" href="{{ url_for_asset('switch.css') }}">
 {% endblock %}
 
+{% macro form_element(form_key, form_details) %}
+<tr>
+  <td class="col-lg-2">
+    <label for="element_{{ form_key }}" control-label="">
+      {%- if "title" in form_details.schema and form_details.schema.title -%}
+        {{ form_details.schema.title }}
+      {%- else -%}
+        {{ form_key }}
+      {%- endif -%}
+      {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type and not "boolean" in form_details.schema.type -%}
+        <strong style="color: red"> *</strong>
+      {%- endif -%}
+    : </label>
+  </td>
+  <td>
+    {% if "custom_html_form" in form_details.schema %}
+      {{ form_details.schema.custom_html_form | replace("{name}", "element_" + form_key) | replace("{value}", form_details.value) }}
+    {% elif "type" in form_details.schema and form_details.schema.type == "boolean" %}
+      <label for="element_{{ form_key }}" control-label="">
+        <input class="switch-input" name="element_{{ form_key }}" id="element_{{ form_key }}" type="checkbox"
+          {%- if form_details.value %} checked="checked"{%- endif -%}/>
+        <span class="switch" aria-hidden="true"></span>
+      </label>
+    {% elif "format" in form_details.schema and "date-time" in form_details.schema.format %}
+      <div class="input-group datetime datetimepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "date" in form_details.schema.format %}
+      <div class="input-group datetime datepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "time" in form_details.schema.format %}
+      <div class="input-group datetime timepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %}
+          value="{% if form_details.value %}{{ form_details.value[0:2] }}00-01-01 {{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "enum" in form_details.schema and form_details.schema.enum %}
+      <select class="my_select2 form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" data-placeholder="Select Value"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}" onchange="updateJSONconf();"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif %}>
+        {% for option in form_details.schema.enum -%}
+        <option>{{ option }}</option>
+        {% endfor -%}
+      </select>
+    {% elif form_details.schema and "array" in form_details.schema.type %}
+      <textarea class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" valuetype="array" rows="6"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif -%}>
+        {%- for txt in form_details.value -%}
+          {{ txt }}{{ "\n" }}
+        {%- endfor -%}
+      </textarea>
+    {% elif form_details.schema and "object" in form_details.schema.type %}
+      <textarea class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" valuetype="object" rows="6"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif -%}>
+        {{- form_details.value | tojson() -}}
+      </textarea>
+    {% elif form_details.schema and ("integer" in form_details.schema.type or "number" in form_details.schema.type) %}
+      <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" placeholder="" valuetype="number" type="number"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}"
+        {%- if form_details.schema.minimum %} min="{{ form_details.schema.minimum }}"{% endif %}
+        {%- if form_details.schema.maximum %} max="{{ form_details.schema.maximum }}"{% endif %}
+        {%- if form_details.schema.type and not "null" in form_details.schema.type %} required=""{% endif %} />
+    {% else %}
+      <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" placeholder="" type="text"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}"
+        {%- if form_details.schema and form_details.schema.minLength %} minlength="{{ form_details.schema.minLength }}"{% endif %}
+        {%- if form_details.schema and form_details.schema.maxLength %} maxlength="{{ form_details.schema.maxLength }}"{% endif %}
+        {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type %} required=""{% endif %} />
+    {% endif %}
+    {% if form_details.description -%}
+      <span class="help-block">{{ form_details.description }}</span>

Review Comment:
   This makes the help text/description either as markup (if it is HTML) or plain text:
   ![image](https://user-images.githubusercontent.com/95105677/214951601-2efa51b2-5787-4351-851f-806706956767.png)
   



##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -27,11 +27,93 @@
   <link rel="stylesheet" type="text/css" href="{{ url_for_asset('switch.css') }}">
 {% endblock %}
 
+{% macro form_element(form_key, form_details) %}
+<tr>
+  <td class="col-lg-2">
+    <label for="element_{{ form_key }}" control-label="">
+      {%- if "title" in form_details.schema and form_details.schema.title -%}
+        {{ form_details.schema.title }}
+      {%- else -%}
+        {{ form_key }}
+      {%- endif -%}
+      {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type and not "boolean" in form_details.schema.type -%}
+        <strong style="color: red"> *</strong>
+      {%- endif -%}
+    : </label>
+  </td>
+  <td>
+    {% if "custom_html_form" in form_details.schema %}
+      {{ form_details.schema.custom_html_form | replace("{name}", "element_" + form_key) | replace("{value}", form_details.value) }}
+    {% elif "type" in form_details.schema and form_details.schema.type == "boolean" %}
+      <label for="element_{{ form_key }}" control-label="">
+        <input class="switch-input" name="element_{{ form_key }}" id="element_{{ form_key }}" type="checkbox"
+          {%- if form_details.value %} checked="checked"{%- endif -%}/>
+        <span class="switch" aria-hidden="true"></span>
+      </label>
+    {% elif "format" in form_details.schema and "date-time" in form_details.schema.format %}
+      <div class="input-group datetime datetimepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "date" in form_details.schema.format %}
+      <div class="input-group datetime datepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "time" in form_details.schema.format %}
+      <div class="input-group datetime timepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %}
+          value="{% if form_details.value %}{{ form_details.value[0:2] }}00-01-01 {{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "enum" in form_details.schema and form_details.schema.enum %}
+      <select class="my_select2 form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" data-placeholder="Select Value"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}" onchange="updateJSONconf();"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif %}>
+        {% for option in form_details.schema.enum -%}
+        <option>{{ option }}</option>
+        {% endfor -%}
+      </select>
+    {% elif form_details.schema and "array" in form_details.schema.type %}
+      <textarea class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" valuetype="array" rows="6"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif -%}>
+        {%- for txt in form_details.value -%}
+          {{ txt }}{{ "\n" }}
+        {%- endfor -%}
+      </textarea>
+    {% elif form_details.schema and "object" in form_details.schema.type %}
+      <textarea class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" valuetype="object" rows="6"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif -%}>
+        {{- form_details.value | tojson() -}}
+      </textarea>
+    {% elif form_details.schema and ("integer" in form_details.schema.type or "number" in form_details.schema.type) %}
+      <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" placeholder="" valuetype="number" type="number"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}"
+        {%- if form_details.schema.minimum %} min="{{ form_details.schema.minimum }}"{% endif %}
+        {%- if form_details.schema.maximum %} max="{{ form_details.schema.maximum }}"{% endif %}
+        {%- if form_details.schema.type and not "null" in form_details.schema.type %} required=""{% endif %} />
+    {% else %}
+      <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" placeholder="" type="text"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}"
+        {%- if form_details.schema and form_details.schema.minLength %} minlength="{{ form_details.schema.minLength }}"{% endif %}
+        {%- if form_details.schema and form_details.schema.maxLength %} maxlength="{{ form_details.schema.maxLength }}"{% endif %}
+        {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type %} required=""{% endif %} />

Review Comment:
   ...aaaand final leftover, if not any specific type then a simple text field is rendered incl. optional min/max length:
   ![image](https://user-images.githubusercontent.com/95105677/214951414-fc24570b-f4fa-44d2-aa65-4fa38c3aa3fa.png)
   



##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -27,11 +27,93 @@
   <link rel="stylesheet" type="text/css" href="{{ url_for_asset('switch.css') }}">
 {% endblock %}
 
+{% macro form_element(form_key, form_details) %}
+<tr>
+  <td class="col-lg-2">
+    <label for="element_{{ form_key }}" control-label="">
+      {%- if "title" in form_details.schema and form_details.schema.title -%}
+        {{ form_details.schema.title }}
+      {%- else -%}
+        {{ form_key }}
+      {%- endif -%}
+      {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type and not "boolean" in form_details.schema.type -%}
+        <strong style="color: red"> *</strong>
+      {%- endif -%}
+    : </label>
+  </td>
+  <td>
+    {% if "custom_html_form" in form_details.schema %}
+      {{ form_details.schema.custom_html_form | replace("{name}", "element_" + form_key) | replace("{value}", form_details.value) }}
+    {% elif "type" in form_details.schema and form_details.schema.type == "boolean" %}
+      <label for="element_{{ form_key }}" control-label="">
+        <input class="switch-input" name="element_{{ form_key }}" id="element_{{ form_key }}" type="checkbox"
+          {%- if form_details.value %} checked="checked"{%- endif -%}/>
+        <span class="switch" aria-hidden="true"></span>
+      </label>
+    {% elif "format" in form_details.schema and "date-time" in form_details.schema.format %}
+      <div class="input-group datetime datetimepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "date" in form_details.schema.format %}
+      <div class="input-group datetime datepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "time" in form_details.schema.format %}
+      <div class="input-group datetime timepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %}
+          value="{% if form_details.value %}{{ form_details.value[0:2] }}00-01-01 {{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "enum" in form_details.schema and form_details.schema.enum %}
+      <select class="my_select2 form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" data-placeholder="Select Value"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}" onchange="updateJSONconf();"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif %}>
+        {% for option in form_details.schema.enum -%}
+        <option>{{ option }}</option>
+        {% endfor -%}
+      </select>
+    {% elif form_details.schema and "array" in form_details.schema.type %}
+      <textarea class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" valuetype="array" rows="6"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif -%}>
+        {%- for txt in form_details.value -%}
+          {{ txt }}{{ "\n" }}
+        {%- endfor -%}
+      </textarea>
+    {% elif form_details.schema and "object" in form_details.schema.type %}
+      <textarea class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" valuetype="object" rows="6"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif -%}>
+        {{- form_details.value | tojson() -}}
+      </textarea>
+    {% elif form_details.schema and ("integer" in form_details.schema.type or "number" in form_details.schema.type) %}
+      <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" placeholder="" valuetype="number" type="number"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}"
+        {%- if form_details.schema.minimum %} min="{{ form_details.schema.minimum }}"{% endif %}
+        {%- if form_details.schema.maximum %} max="{{ form_details.schema.maximum }}"{% endif %}
+        {%- if form_details.schema.type and not "null" in form_details.schema.type %} required=""{% endif %} />

Review Comment:
   If the data type is a number then a HTML number field is generated - see the spinner on the right side. Min/Max can be checked here optionally:
   ![image](https://user-images.githubusercontent.com/95105677/214951278-c990187a-d06f-476e-aafe-9ba8a3e05fd9.png)
   



##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -47,22 +129,90 @@ <h2>Trigger DAG: {{ dag_id }}</h2>
         <input type="text" class="form-control" placeholder="Run ID" name="run_id">
       </div>
     </div>
+    {% if recent_confs|length > 0 %}
     <div class="form-group row">
       <div class="col-md-2">
-        <label for="recent_configs">Recent Configurations</label>
+        <label for="recent_configs">Select Recent Configurations</label>
         <select class="form-control" name="recent_configs" id="recent_configs">
-          <option value="">empty config</option>
-          {% for conf in recent_confs %}
-          <option value="{{ conf }}">{{ conf }}</option>
+          <option value="{{ conf }}">Default parameters</option>
+          {% for run_id, recent_conf in recent_confs.items() %}
+          <option value="{{ recent_conf }}">{{ run_id }}: {{ recent_conf }}</option>
           {% endfor %}

Review Comment:
   This section is the select of the recent configs which can be applied to the form:
   ![image](https://user-images.githubusercontent.com/95105677/214951905-5b12c900-106f-4f71-91b6-941401d2746f.png)
   



##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -47,22 +129,90 @@ <h2>Trigger DAG: {{ dag_id }}</h2>
         <input type="text" class="form-control" placeholder="Run ID" name="run_id">
       </div>
     </div>
+    {% if recent_confs|length > 0 %}
     <div class="form-group row">
       <div class="col-md-2">
-        <label for="recent_configs">Recent Configurations</label>
+        <label for="recent_configs">Select Recent Configurations</label>
         <select class="form-control" name="recent_configs" id="recent_configs">
-          <option value="">empty config</option>
-          {% for conf in recent_confs %}
-          <option value="{{ conf }}">{{ conf }}</option>
+          <option value="{{ conf }}">Default parameters</option>
+          {% for run_id, recent_conf in recent_confs.items() %}
+          <option value="{{ recent_conf }}">{{ run_id }}: {{ recent_conf }}</option>
           {% endfor %}
         </select>
       </div>
     </div>
-    <label for="conf">Configuration JSON (Optional, must be a dict object)</label>
-    <textarea class="form-control" name="conf" id="json">{{ conf }}</textarea>
+    {% endif %}
+    {%- if form_fields %}
+    <div class="panel panel-primary ">
+      <div class="panel-heading">
+        <h4 class="panel-title">DAG conf Parameters</h4>
+      </div>
+      <textarea style="display: none;" id="json_start" name="json_start">
+        {{- conf -}}
+      </textarea>
+      <textarea style="display: none;" id="form_sections" name="form_sections">
+        {{- form_fields.values() | map(attribute="schema.section", default="") | reject("equalto", "") | unique() | join(",") | lower() | replace(" ", "_") -}}
+      </textarea>
+      <div class="panel-body">
 
-    <br />
+        <table class="table table-hover table-bordered">
+          <tbody>
+            {% for form_key, form_details in form_fields.items() %}
+            {% if ("section" not in form_details.schema or not form_details.schema.section) and ("const" not in form_details.schema or not form_details.schema.const) %}
+            {{ form_element(form_key, form_details) }}
+            {% endif %}
+            {% endfor %}
+          </tbody>
+        </table>
 
+        {% for form_section, form_items in form_fields.values() | groupby(attribute="schema.section", default="") %}
+        {% if form_section %}
+        <div class="panel-group" id="accordion_section_{{ form_section | lower() | replace(' ', '_') }}">
+          <div class="panel panel-default">
+            <div class="panel-heading">
+              <h4 class="panel-title">
+                <a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion_section_{{ form_section | lower() | replace(' ', '_') }}" href="#accordion_section_{{ form_section | lower() | replace(' ', '_') }}_href"
+                  data-original-title="" title="" id="{{ form_section | lower() | replace(' ', '_') }}_toggle">{{ form_section }}<span class="caret"></span></a>
+              </h4>
+            </div>
+            <div id="accordion_section_{{ form_section | lower() | replace(' ', '_') }}_href" class="panel-collapse collapse">
+              <div class="panel-body">
+                <table class="table table-hover table-bordered">
+                  <tbody>
+                    {% for form_key, form_details in form_fields.items() %}
+                    {% if form_details.schema.section == form_section and not form_details.schema.const %}
+                    {{ form_element(form_key, form_details) }}
+                    {% endif %}
+                    {% endfor %}
+                  </tbody>
+                </table>
+              </div>
+            </div>
+          </div>
+        </div>
+        {% endif %}
+        {% endfor %}
+
+        <div class="panel-group" id="accordion_json_conf">
+          <div class="panel panel-default">
+            <div class="panel-heading">
+              <h4 class="panel-title">
+                <a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion_json_conf" href="#accordion_json_conf_href" data-original-title="" title="" id="generated_json_toggle">Generated Configuration JSON<span class="caret"></span></a>
+              </h4>
+            </div>
+            <div id="accordion_json_conf_href" class="panel-collapse collapse">
+              <div class="panel-body">
+                <textarea name="conf" id="json">{{ conf }}</textarea>
+              </div>
+            </div>
+          </div>
+        </div>

Review Comment:
   Below the form the generated JSON can be seen - if you want to re-use this for future reference:
   ![image](https://user-images.githubusercontent.com/95105677/214952362-28966bd8-423a-48ce-8440-c309d3e439b0.png)
   



##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -47,22 +129,90 @@ <h2>Trigger DAG: {{ dag_id }}</h2>
         <input type="text" class="form-control" placeholder="Run ID" name="run_id">
       </div>
     </div>
+    {% if recent_confs|length > 0 %}
     <div class="form-group row">
       <div class="col-md-2">
-        <label for="recent_configs">Recent Configurations</label>
+        <label for="recent_configs">Select Recent Configurations</label>
         <select class="form-control" name="recent_configs" id="recent_configs">
-          <option value="">empty config</option>
-          {% for conf in recent_confs %}
-          <option value="{{ conf }}">{{ conf }}</option>
+          <option value="{{ conf }}">Default parameters</option>
+          {% for run_id, recent_conf in recent_confs.items() %}
+          <option value="{{ recent_conf }}">{{ run_id }}: {{ recent_conf }}</option>
           {% endfor %}
         </select>
       </div>
     </div>
-    <label for="conf">Configuration JSON (Optional, must be a dict object)</label>
-    <textarea class="form-control" name="conf" id="json">{{ conf }}</textarea>
+    {% endif %}
+    {%- if form_fields %}
+    <div class="panel panel-primary ">
+      <div class="panel-heading">
+        <h4 class="panel-title">DAG conf Parameters</h4>
+      </div>
+      <textarea style="display: none;" id="json_start" name="json_start">
+        {{- conf -}}
+      </textarea>
+      <textarea style="display: none;" id="form_sections" name="form_sections">
+        {{- form_fields.values() | map(attribute="schema.section", default="") | reject("equalto", "") | unique() | join(",") | lower() | replace(" ", "_") -}}
+      </textarea>
+      <div class="panel-body">
 
-    <br />
+        <table class="table table-hover table-bordered">
+          <tbody>
+            {% for form_key, form_details in form_fields.items() %}
+            {% if ("section" not in form_details.schema or not form_details.schema.section) and ("const" not in form_details.schema or not form_details.schema.const) %}
+            {{ form_element(form_key, form_details) }}
+            {% endif %}
+            {% endfor %}
+          </tbody>
+        </table>
 
+        {% for form_section, form_items in form_fields.values() | groupby(attribute="schema.section", default="") %}
+        {% if form_section %}
+        <div class="panel-group" id="accordion_section_{{ form_section | lower() | replace(' ', '_') }}">
+          <div class="panel panel-default">
+            <div class="panel-heading">
+              <h4 class="panel-title">
+                <a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion_section_{{ form_section | lower() | replace(' ', '_') }}" href="#accordion_section_{{ form_section | lower() | replace(' ', '_') }}_href"
+                  data-original-title="" title="" id="{{ form_section | lower() | replace(' ', '_') }}_toggle">{{ form_section }}<span class="caret"></span></a>
+              </h4>
+            </div>

Review Comment:
   If the form defines multiple sections, this generate a new section header - each section is folded by default and can be un-folded:
   ![image](https://user-images.githubusercontent.com/95105677/214952183-4a9f23bc-4b71-409d-8888-4001cf6d93b3.png)
   



##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -27,11 +27,93 @@
   <link rel="stylesheet" type="text/css" href="{{ url_for_asset('switch.css') }}">
 {% endblock %}
 
+{% macro form_element(form_key, form_details) %}
+<tr>
+  <td class="col-lg-2">
+    <label for="element_{{ form_key }}" control-label="">
+      {%- if "title" in form_details.schema and form_details.schema.title -%}
+        {{ form_details.schema.title }}
+      {%- else -%}
+        {{ form_key }}
+      {%- endif -%}
+      {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type and not "boolean" in form_details.schema.type -%}
+        <strong style="color: red"> *</strong>
+      {%- endif -%}
+    : </label>
+  </td>
+  <td>
+    {% if "custom_html_form" in form_details.schema %}
+      {{ form_details.schema.custom_html_form | replace("{name}", "element_" + form_key) | replace("{value}", form_details.value) }}
+    {% elif "type" in form_details.schema and form_details.schema.type == "boolean" %}
+      <label for="element_{{ form_key }}" control-label="">
+        <input class="switch-input" name="element_{{ form_key }}" id="element_{{ form_key }}" type="checkbox"
+          {%- if form_details.value %} checked="checked"{%- endif -%}/>
+        <span class="switch" aria-hidden="true"></span>
+      </label>
+    {% elif "format" in form_details.schema and "date-time" in form_details.schema.format %}
+      <div class="input-group datetime datetimepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "date" in form_details.schema.format %}
+      <div class="input-group datetime datepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>

Review Comment:
   Date picker (without time):
   ![image](https://user-images.githubusercontent.com/95105677/214950428-e61454c4-5932-499f-8f07-41090500da27.png)
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] bbovenzi commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "bbovenzi (via GitHub)" <gi...@apache.org>.
bbovenzi commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1084258122


##########
airflow/www/static/js/trigger.js:
##########
@@ -19,33 +19,190 @@
 
 /* global document, CodeMirror, window */
 
-const textArea = document.getElementById('json');
+let jsonForm;
+const objectFields = new Map();
 const recentConfigList = document.getElementById('recent_configs');
-const minHeight = 300;
-const maxHeight = window.innerHeight - 450;
-const height = maxHeight > minHeight ? maxHeight : minHeight;
-
-CodeMirror.fromTextArea(textArea, {
-  lineNumbers: true,
-  mode: {
-    name: 'javascript',
-    json: true,
-  },
-  gutters: ['CodeMirror-lint-markers'],
-  lint: true,
-})
-  .setSize(null, height);
+
+/**
+ * Update the generated JSON DagRun.conf JSON field if any field changed
+ */
+function updateJSONconf() {
+  const jsonStart = document.getElementById('json_start').value;
+  const params = JSON.parse(jsonStart);
+  const elements = document.getElementById('trigger_form');
+  for (let i = 0; i < elements.length; i += 1) {
+    if (elements[i].name && elements[i].name.startsWith('element_')) {
+      const keyName = elements[i].name.substr(8);
+      if (elements[i].type === 'checkbox') {
+        params[keyName] = elements[i].checked;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'array') {
+        const lines = elements[i].value.split('\n');
+        const values = [];
+        for (let j = 0; j < lines.length; j += 1) {
+          if (lines[j].trim().length > 0) {
+            values[values.length] = lines[j].trim();
+          }
+        }
+        params[keyName] = values;
+      } else if (elements[i].value.length === 0) {
+        params[keyName] = null;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'object') {
+        try {
+          const textValue = objectFields.get(elements[i].name).getValue();
+          if (textValue.length > 0) {
+            const objValue = JSON.parse(textValue);
+            params[keyName] = objValue;
+            objectFields.get(elements[i].name).setValue(JSON.stringify(objValue, null, 4));
+          } else {
+            params[keyName] = null;
+          }
+        } catch (e) {
+          // ignore JSON parsing errors
+        }
+      } else if (Number.isNaN(elements[i].value)) {
+        params[keyName] = elements[i].value;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'number') {
+        params[keyName] = Number(elements[i].value);
+      } else {
+        params[keyName] = elements[i].value;
+      }
+    }
+  }
+  jsonForm.setValue(JSON.stringify(params, null, 4));
+}
+
+/**
+ * Initialize the form during load of the web page
+ */
+function initForm() {
+  const formSectionsElement = document.getElementById('form_sections');
+  const formWithFields = (formSectionsElement != null);
+
+  // Initialize the Generated JSON form or JSON entry form
+  const minHeight = 300;
+  const maxHeight = (formWithFields ? window.innerHeight / 2 : window.innerHeight) - 550;
+  const height = maxHeight > minHeight ? maxHeight : minHeight;
+  jsonForm = CodeMirror.fromTextArea(document.getElementById('json'), {
+    lineNumbers: true,
+    mode: { name: 'javascript', json: true },
+    gutters: ['CodeMirror-lint-markers'],
+    lint: true,
+  });
+  jsonForm.setSize(null, height);
+
+  if (formWithFields) {
+    // Apply JSON formatting and linting to all object fields in the form
+    const elements = document.getElementById('trigger_form');
+    for (let i = 0; i < elements.length; i += 1) {
+      if (elements[i].name && elements[i].name.startsWith('element_')) {
+        if (elements[i].attributes.valuetype
+            && elements[i].attributes.valuetype.value === 'object') {
+          const field = CodeMirror.fromTextArea(elements[i], {
+            lineNumbers: true,
+            mode: { name: 'javascript', json: true },
+            gutters: ['CodeMirror-lint-markers'],
+            lint: true,
+          });
+          /* eslint-disable no-unused-vars */
+          field.on('blur', (cm, change) => { updateJSONconf(); });

Review Comment:
   ```suggestion
             field.on('blur', updateJSONconf);
   ```
   I think this should remove the need for the `eslint-disable`



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] mayeulk commented on pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "mayeulk (via GitHub)" <gi...@apache.org>.
mayeulk commented on PR #27063:
URL: https://github.com/apache/airflow/pull/27063#issuecomment-1490338685

   Hello @pierrejeambrun,
   Thank you so much for the swift and detailed answer; it helps me a lot!
   I'll keep an eye on https://airflow.apache.org/announcements/ and  the 2.6.0 milestone https://github.com/apache/airflow/milestone/67
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] ashb commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "ashb (via GitHub)" <gi...@apache.org>.
ashb commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1083797766


##########
airflow/example_dags/example_params_ui_tutorial.py:
##########
@@ -0,0 +1,234 @@
+#
+# 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.
+"""DAG demonstrating various options for a trigger form generated by DAG params.
+
+The DAG attribute `params` is used to define a default dictionary of parameters which are usually passed
+to the DAG and which are used to render a trigger form.
+"""
+from __future__ import annotations
+
+import datetime
+import json
+from pathlib import Path
+
+from airflow import DAG
+from airflow.decorators import task
+from airflow.exceptions import AirflowSkipException
+from airflow.models.dagrun import DagRun
+from airflow.models.param import Param
+from airflow.models.taskinstance import TaskInstance
+
+HTML_COLOR_PICKER_CUSTOM_CODE = """
+<table width="100%" cellspacing="5"><tbody><tr><td>
+    <label for="r_{name}">Red:</label>
+</td><td width="80%">
+    <input id="r_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
+</td><td rowspan="3" style="padding-left: 10px;">
+    <div id="preview_{name}"
+      style="line-height: 40px; margin-bottom: 7px; width: 100%; background-color: {value};">&nbsp;</div>
+    <input class="form-control" type="text" maxlength="7" id="{name}" name="{name}" value="{value}"
+      onchange="v_{name}()" />
+</td></tr><tr><td>
+    <label for="g_{name}">Green:</label>
+</td><td>
+    <input id="g_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
+</td></tr><tr><td>
+    <label for="b_{name}">Blue:</label>
+</td><td>
+    <input id="b_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
+</td></tr></tbody></table>
+<script lang="javascript">
+    const hex_chars = "0123456789ABCDEF";
+    function i2hex(name) {
+        var i = document.getElementById(name).value;
+        return hex_chars.substr(parseInt(i / 16), 1) + hex_chars.substr(parseInt(i % 16), 1)
+    }
+    function u_{name}() {
+        document.getElementById("{name}").value = "#"+i2hex("r_{name}")+i2hex("g_{name}")+i2hex("b_{name}");
+        document.getElementById("preview_{name}").style.background = document.getElementById("{name}").value;
+        updateJSONconf();
+    }
+    function hex2i(text) {
+        return hex_chars.indexOf(text.substr(0,1)) * 16 + hex_chars.indexOf(text.substr(1,1));
+    }
+    function v_{name}() {
+        var value = document.getElementById("{name}").value.toUpperCase();
+        document.getElementById("r_{name}").value = hex2i(value.substr(1,2));
+        document.getElementById("g_{name}").value = hex2i(value.substr(3,2));
+        document.getElementById("b_{name}").value = hex2i(value.substr(5,2));
+        document.getElementById("preview_{name}").style.background = document.getElementById("{name}").value;
+    }
+    v_{name}();
+</script>
+"""
+
+with DAG(
+    dag_id=Path(__file__).stem,
+    description=__doc__[0 : __doc__.find(".")],
+    doc_md=__doc__,
+    schedule=None,
+    start_date=datetime.datetime(2022, 3, 4),
+    catchup=False,
+    tags=["example_ui"],
+    params={
+        # Let's start simple: Standard dict values are detected from type and offered as entry form fields.
+        # Detected types are numbers, text, boolean, lists and dicts.
+        # Note that such auto-detected parameters are treated as optional (not required to contain a value)
+        "x": 3,
+        "text": "Hello World!",
+        "flag": False,
+        "a_simple_list": ["one", "two", "three", "actually one value is made per line"],
+        # But of course you might want to have it nicer! Let's add some description to parameters.
+        # Note if you can add any HTML formatting to the description, you need to use the description_html
+        # attribute.
+        "most_loved_number": Param(
+            42,
+            type="integer",
+            title="You 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
+            <a href='https://en.wikipedia.org/wiki/Phrases_from_The_Hitchhiker%27s_Guide_to_the_Galaxy#
+            The_Answer_to_the_Ultimate_Question_of_Life,_the_Universe,_and_Everything_is_42'>
+            The Hitchhiker's Guide to the Galaxy</a>""",
+        ),
+        # If you want to have a selection list box then you can use the enum feature of JSON schema
+        "pick_one": Param(
+            "value 1",
+            type="string",
+            title="Select one Value",
+            description="You can use JSON schema enum's to generate drop down selection boxes.",
+            enum=[f"value {i}" for i in range(1, 42)],
+        ),
+        # Boolean as proper parameter with description
+        "bool": Param(
+            True,
+            type="boolean",
+            title="Please confirm",
+            description="A On/Off selection with a proper description.",
+        ),
+        # Dates and Times are also supported
+        "date_time": Param(
+            f"{datetime.date.today()} {datetime.time(hour=12, minute=17, second=00)}",
+            type="string",
+            format="date-time",
+            title="Date-Time Picker",
+            description="Please select a date and time, use the button on the left for a pup-up calendar.",
+        ),
+        "date": Param(
+            f"{datetime.date.today()}",
+            type="string",
+            format="date",
+            title="Date Picker",
+            description="Please select a date, use the button on the left for a pup-up calendar. "
+            "See that here are no times!",
+        ),
+        "time": Param(
+            f"{datetime.time(hour=12, minute=13, second=14)}",
+            type=["string", "null"],
+            format="time",
+            title="Time Picker",
+            description="Please select a time, use the button on the left for a pup-up tool.",
+        ),
+        # Fields can be required or not. If the defined fields are typed they are getting required by default
+        # (else they would not pass JSON schema validation) - to make typed fields optional you must
+        # permit the optional "null" type
+        "required_field": Param(
+            "You can not trigger if no text is given here!",
+            type="string",
+            title="Required text field",
+            description="This field is required. You can not submit without having text in here.",
+        ),
+        "optional_field": Param(
+            "optional text, you can trigger also w/o text",
+            type=["null", "string"],
+            title="Optional text field",
+            description_html="This field is optional. As field content is JSON schema validated you must "
+            "allow the <code>null</code> type.",
+        ),
+        # You can arrange the entry fields in sections so that you can have a better overview for the user
+        # Therefore you can add the "section" attribute.
+        # The benefit of the Params class definition is that the full scope of JSON schema validation
+        # can be leveraged for form fields and they will be validated before DAG submission.
+        "checked_text": Param(
+            "length-checked-field",
+            type="string",
+            title="Text field with length check",
+            description_html="""This field is required. And you need to provide something between 10 and 30
+            characters. See the
+            <a href='https://json-schema.org/understanding-json-schema/reference/string.html'>
+            JSON schema description (string)</a> in for more details""",
+            minLength=10,
+            maxLength=20,
+            section="JSON Schema validation options",
+        ),
+        "checked_number": Param(
+            100,
+            type="number",
+            title="Number field with value check",
+            description_html="""This field is required. You need to provide any number between 64 and 128.
+            See the <a href='https://json-schema.org/understanding-json-schema/reference/numeric.html'>
+            JSON schema description (numbers)</a> in for more details""",
+            minimum=64,
+            maximum=128,
+            section="JSON Schema validation options",
+        ),
+        # Some further cool stuff as advanced options are also possible
+        # You can have the user entering a dict object as a JSON with validation
+        "object": Param(
+            {"key": "value"},
+            type=["object", "null"],
+            title="JSON entry field",
+            section="Special advanced stuff with form fields",
+        ),
+        # If you want to have static parameters which are always passed and not editable by the user
+        # then you can use the JSON schema option of passing constant values. These parameters
+        # will not be displayed but passed to the DAG
+        "hidden_secret_field": Param("constant value", const="constant value"),
+        # Finally besides the standard provided field generator you can have you own HTML form code
+        # injected - but be careful, you can also mess-up the layout!
+        "color_picker": Param(
+            "#FF8800",
+            type="string",
+            title="Pick a color",
+            description_html="""This is a special HTML widget as custom implementation in the DAG code.
+            It is templated with the following parameter to render proper HTML form fields:
+            <ul>
+                <li><code>{name}</code>: Name of the HTML input field that is expected.</li>
+                <li><code>{value}</code>:
+                    (Default) value that should be displayed when showing/loading the form.</li>
+                <li>Note: If you have elements changing a value, call <code>updateJSONconf()</code>.</li>
+            </ul>
+            Example: <code>&lt;input name='{name}' value='{value}' onchange='updateJSONconf()' /&gt;</code>
+            """,
+            custom_html_form=HTML_COLOR_PICKER_CUSTOM_CODE,
+            section="Special advanced stuff with form fields",
+        ),
+    },
+) as dag:
+
+    @task(task_id="show_params")
+    def show_params(**kwargs) -> None:
+        ti: TaskInstance = kwargs["ti"]
+        dag_run: DagRun = ti.dag_run
+        if not dag_run.conf:
+            print("Uups, no parameters supplied as DagRun.conf, was the trigger w/o form?")
+            raise AirflowSkipException("No DagRun.conf parameters supplied.")
+        print(f"This DAG was triggert with the following parameters:\n{json.dumps(dag_run.conf, indent=4)}")

Review Comment:
   ```suggestion
           print(f"This DAG was triggered with the following parameters:\n{json.dumps(dag_run.conf, indent=4)}")
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] maxnathaniel commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "maxnathaniel (via GitHub)" <gi...@apache.org>.
maxnathaniel commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1086383535


##########
airflow/www/static/js/trigger.js:
##########
@@ -19,33 +19,190 @@
 
 /* global document, CodeMirror, window */
 
-const textArea = document.getElementById('json');
+let jsonForm;
+const objectFields = new Map();
 const recentConfigList = document.getElementById('recent_configs');
-const minHeight = 300;
-const maxHeight = window.innerHeight - 450;
-const height = maxHeight > minHeight ? maxHeight : minHeight;
-
-CodeMirror.fromTextArea(textArea, {
-  lineNumbers: true,
-  mode: {
-    name: 'javascript',
-    json: true,
-  },
-  gutters: ['CodeMirror-lint-markers'],
-  lint: true,
-})
-  .setSize(null, height);
+
+/**
+ * Update the generated JSON DagRun.conf JSON field if any field changed
+ */
+function updateJSONconf() {
+  const jsonStart = document.getElementById('json_start').value;
+  const params = JSON.parse(jsonStart);
+  const elements = document.getElementById('trigger_form');
+  for (let i = 0; i < elements.length; i += 1) {
+    if (elements[i].name && elements[i].name.startsWith('element_')) {
+      const keyName = elements[i].name.substr(8);
+      if (elements[i].type === 'checkbox') {
+        params[keyName] = elements[i].checked;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'array') {
+        const lines = elements[i].value.split('\n');
+        const values = [];
+        for (let j = 0; j < lines.length; j += 1) {
+          if (lines[j].trim().length > 0) {
+            values[values.length] = lines[j].trim();
+          }
+        }
+        params[keyName] = values;
+      } else if (elements[i].value.length === 0) {
+        params[keyName] = null;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'object') {
+        try {
+          const textValue = objectFields.get(elements[i].name).getValue();
+          if (textValue.length > 0) {
+            const objValue = JSON.parse(textValue);
+            params[keyName] = objValue;
+            objectFields.get(elements[i].name).setValue(JSON.stringify(objValue, null, 4));
+          } else {
+            params[keyName] = null;
+          }
+        } catch (e) {
+          // ignore JSON parsing errors
+        }
+      } else if (Number.isNaN(elements[i].value)) {
+        params[keyName] = elements[i].value;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'number') {
+        params[keyName] = Number(elements[i].value);
+      } else {
+        params[keyName] = elements[i].value;
+      }
+    }
+  }
+  jsonForm.setValue(JSON.stringify(params, null, 4));
+}
+
+/**
+ * Initialize the form during load of the web page
+ */
+function initForm() {
+  const formSectionsElement = document.getElementById('form_sections');
+  const formWithFields = (formSectionsElement != null);
+
+  // Initialize the Generated JSON form or JSON entry form
+  const minHeight = 300;
+  const maxHeight = (formWithFields ? window.innerHeight / 2 : window.innerHeight) - 550;
+  const height = maxHeight > minHeight ? maxHeight : minHeight;
+  jsonForm = CodeMirror.fromTextArea(document.getElementById('json'), {
+    lineNumbers: true,
+    mode: { name: 'javascript', json: true },
+    gutters: ['CodeMirror-lint-markers'],
+    lint: true,
+  });
+  jsonForm.setSize(null, height);
+
+  if (formWithFields) {
+    // Apply JSON formatting and linting to all object fields in the form
+    const elements = document.getElementById('trigger_form');
+    for (let i = 0; i < elements.length; i += 1) {
+      if (elements[i].name && elements[i].name.startsWith('element_')) {
+        if (elements[i].attributes.valuetype
+            && elements[i].attributes.valuetype.value === 'object') {
+          const field = CodeMirror.fromTextArea(elements[i], {
+            lineNumbers: true,
+            mode: { name: 'javascript', json: true },
+            gutters: ['CodeMirror-lint-markers'],
+            lint: true,
+          });
+          /* eslint-disable no-unused-vars */
+          field.on('blur', (cm, change) => { updateJSONconf(); });

Review Comment:
   If this is changed to the suggested line, lines 112, 116 and 164 should be updated too



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] ashb commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by GitBox <gi...@apache.org>.
ashb commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1066756429


##########
airflow/www/views.py:
##########
@@ -1955,7 +1963,32 @@ def trigger(self, session=None):
         request_execution_date = request.values.get("execution_date", default=timezone.utcnow().isoformat())
         is_dag_run_conf_overrides_params = conf.getboolean("core", "dag_run_conf_overrides_params")
         dag = get_airflow_app().dag_bag.get_dag(dag_id)
-        dag_orm = session.query(DagModel).filter(DagModel.dag_id == dag_id).first()
+        dag_orm: DagModel = session.query(DagModel).filter(DagModel.dag_id == dag_id).first()
+
+        # Prepare form fields with param struct details to render a proper form with schema information
+        form_fields = {}
+        for k, v in dag.params.items():
+            form_fields[k] = v.dump()
+            # If no schema is provided, auto-detect on default values
+            if "schema" not in form_fields[k]:
+                form_fields[k]["schema"] = {}
+            if "type" not in form_fields[k]["schema"]:
+                if isinstance(form_fields[k]["value"], bool):
+                    form_fields[k]["schema"]["type"] = "boolean"
+                elif isinstance(form_fields[k]["value"], int):
+                    form_fields[k]["schema"]["type"] = ["integer", "null"]
+                elif isinstance(form_fields[k]["value"], list):
+                    form_fields[k]["schema"]["type"] = ["array", "null"]
+                elif isinstance(form_fields[k]["value"], dict):
+                    form_fields[k]["schema"]["type"] = ["object", "null"]
+            # Mark markup fields as safe
+            if "description" in form_fields[k] and form_fields[k]["description"]:
+                form_fields[k]["description"] = Markup(form_fields[k]["description"])

Review Comment:
   I don't think this is safe -- it means of someone puts a description of "field must be < x" it'd break. Using HTML is the exception in my mind, so make the users use this if they want to include HTML



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] bbovenzi commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "bbovenzi (via GitHub)" <gi...@apache.org>.
bbovenzi commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1084245521


##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -27,11 +27,93 @@
   <link rel="stylesheet" type="text/css" href="{{ url_for_asset('switch.css') }}">
 {% endblock %}
 
+{% macro form_element(form_key, form_details) %}
+<tr>
+  <td class="col-lg-2">
+    <label for="element_{{ form_key }}" control-label="">
+      {%- if "title" in form_details.schema and form_details.schema.title -%}
+        {{ form_details.schema.title }}
+      {%- else -%}
+        {{ form_key }}
+      {%- endif -%}
+      {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type and not "boolean" in form_details.schema.type -%}
+        <strong style="color: red"> *</strong>
+      {%- endif -%}
+    : </label>
+  </td>
+  <td>
+    {% if "custom_html_form" in form_details.schema %}
+      {{ form_details.schema.custom_html_form | replace("{name}", "element_" + form_key) | replace("{value}", form_details.value) }}
+    {% elif "type" in form_details.schema and form_details.schema.type == "boolean" %}
+      <label for="element_{{ form_key }}" control-label="">
+        <input class="switch-input" name="element_{{ form_key }}" id="element_{{ form_key }}" type="checkbox"
+          {%- if form_details.value %} checked="checked"{%- endif -%}/>
+        <span class="switch" aria-hidden="true"></span>
+      </label>
+    {% elif "format" in form_details.schema and "date-time" in form_details.schema.format %}
+      <div class="input-group datetime datetimepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "date" in form_details.schema.format %}
+      <div class="input-group datetime datepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "time" in form_details.schema.format %}
+      <div class="input-group datetime timepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %}
+          value="{% if form_details.value %}{{ form_details.value[0:2] }}00-01-01 {{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "enum" in form_details.schema and form_details.schema.enum %}
+      <select class="my_select2 form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" data-placeholder="Select Value"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}" onchange="updateJSONconf();"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif %}>
+        {% for option in form_details.schema.enum -%}
+        <option>{{ option }}</option>
+        {% endfor -%}
+      </select>
+    {% elif form_details.schema and "array" in form_details.schema.type %}
+      <textarea class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" valuetype="array" rows="6"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif -%}>
+        {%- for txt in form_details.value -%}
+          {{ txt }}{{ "\n" }}
+        {%- endfor -%}
+      </textarea>
+    {% elif form_details.schema and "object" in form_details.schema.type %}
+      <textarea class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" valuetype="object" rows="6"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif -%}>
+        {{- form_details.value | tojson() -}}
+      </textarea>
+    {% elif form_details.schema and ("integer" in form_details.schema.type or "number" in form_details.schema.type) %}
+      <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" placeholder="" valuetype="number" type="number"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}"
+        {%- if form_details.schema.minimum %} min="{{ form_details.schema.minimum }}"{% endif %}
+        {%- if form_details.schema.maximum %} max="{{ form_details.schema.maximum }}"{% endif %}
+        {%- if form_details.schema.type and not "null" in form_details.schema.type %} required=""{% endif %} />
+    {% else %}
+      <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" placeholder="" type="text"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}"
+        {%- if form_details.schema and form_details.schema.minLength %} minlength="{{ form_details.schema.minLength }}"{% endif %}
+        {%- if form_details.schema and form_details.schema.maxLength %} maxlength="{{ form_details.schema.maxLength }}"{% endif %}
+        {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type %} required=""{% endif %} />
+    {% endif %}
+    {% if form_details.description -%}

Review Comment:
   Could you include some screenshots of what all this looks like?
   I _feel_ like the description would be best above all of these fields. 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] bbovenzi commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "bbovenzi (via GitHub)" <gi...@apache.org>.
bbovenzi commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1084254664


##########
airflow/www/static/js/trigger.js:
##########
@@ -19,33 +19,190 @@
 
 /* global document, CodeMirror, window */
 
-const textArea = document.getElementById('json');
+let jsonForm;
+const objectFields = new Map();
 const recentConfigList = document.getElementById('recent_configs');
-const minHeight = 300;
-const maxHeight = window.innerHeight - 450;
-const height = maxHeight > minHeight ? maxHeight : minHeight;
-
-CodeMirror.fromTextArea(textArea, {
-  lineNumbers: true,
-  mode: {
-    name: 'javascript',
-    json: true,
-  },
-  gutters: ['CodeMirror-lint-markers'],
-  lint: true,
-})
-  .setSize(null, height);
+
+/**
+ * Update the generated JSON DagRun.conf JSON field if any field changed
+ */
+function updateJSONconf() {
+  const jsonStart = document.getElementById('json_start').value;
+  const params = JSON.parse(jsonStart);
+  const elements = document.getElementById('trigger_form');
+  for (let i = 0; i < elements.length; i += 1) {
+    if (elements[i].name && elements[i].name.startsWith('element_')) {
+      const keyName = elements[i].name.substr(8);
+      if (elements[i].type === 'checkbox') {
+        params[keyName] = elements[i].checked;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'array') {
+        const lines = elements[i].value.split('\n');
+        const values = [];
+        for (let j = 0; j < lines.length; j += 1) {
+          if (lines[j].trim().length > 0) {
+            values[values.length] = lines[j].trim();
+          }
+        }
+        params[keyName] = values;
+      } else if (elements[i].value.length === 0) {
+        params[keyName] = null;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'object') {
+        try {
+          const textValue = objectFields.get(elements[i].name).getValue();
+          if (textValue.length > 0) {
+            const objValue = JSON.parse(textValue);
+            params[keyName] = objValue;
+            objectFields.get(elements[i].name).setValue(JSON.stringify(objValue, null, 4));
+          } else {
+            params[keyName] = null;
+          }
+        } catch (e) {
+          // ignore JSON parsing errors
+        }
+      } else if (Number.isNaN(elements[i].value)) {
+        params[keyName] = elements[i].value;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'number') {
+        params[keyName] = Number(elements[i].value);
+      } else {
+        params[keyName] = elements[i].value;
+      }
+    }
+  }
+  jsonForm.setValue(JSON.stringify(params, null, 4));
+}
+
+/**
+ * Initialize the form during load of the web page
+ */
+function initForm() {
+  const formSectionsElement = document.getElementById('form_sections');
+  const formWithFields = (formSectionsElement != null);

Review Comment:
   ```suggestion
     const formHasFields = (formSectionsElement != null);
   ```
   Just nitpicking.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] mayeulk commented on pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "mayeulk (via GitHub)" <gi...@apache.org>.
mayeulk commented on PR #27063:
URL: https://github.com/apache/airflow/pull/27063#issuecomment-1488860583

   Thank you for this!!
   **Is there a way to have this new function in a docker stack?**
   What would be the process? Taking the version of the latest commit from main (like: 26 January, aaf54fc5d472f0a727f9df68f936fde91792a091 )? (with git pull?)
   Or is it planned to be in 2.5.3 or 2.5.4?
   (I cannot see it in https://github.com/apache/airflow/milestone/70 ).
   
   Or the only reasonable way would be waiting until it goes into a stable release?
   
   I'm running Airflow 2.5.1 with Docker Compose in dev mode, following:
   https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html
   (I can see there is a slightly different docker-compose.yaml for 2.5.2)
   
   Thank you.
   Mayeul


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] jens-scheffler-bosch commented on pull request #27063: Feature/26215 AIP-50 Trigger UI based on FAB

Posted by GitBox <gi...@apache.org>.
jens-scheffler-bosch commented on PR #27063:
URL: https://github.com/apache/airflow/pull/27063#issuecomment-1374909603

   > > Now time for a final review, then (in my view) ready to be merged.
   > 
   > I don't see vote thread on the mailing list
   
   @eladkal thanks for helping out in understanding the voting process - VOTE is done now - so ready to be reviewed (before PR getting stale second time)... @pierrejeambrun can you have a review or propose somebody else taking a look? I would love to get this into 2.6.0 :-D


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] jens-scheffler-bosch commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by GitBox <gi...@apache.org>.
jens-scheffler-bosch commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1070095775


##########
airflow/example_dags/example_params_ui_tutorial.py:
##########
@@ -0,0 +1,232 @@
+#
+# 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.
+"""DAG demonstrating various options for a trigger form generated by DAG params.
+
+The DAG attribute `params` is used to define a default dictionary of parameters which are usually passed
+to the DAG and which are used to render a trigger form.
+"""
+from __future__ import annotations
+
+import datetime
+import json
+from pathlib import Path
+
+from airflow import DAG
+from airflow.decorators import task
+from airflow.exceptions import AirflowSkipException
+from airflow.models.dagrun import DagRun
+from airflow.models.param import Param
+from airflow.models.taskinstance import TaskInstance
+
+HTML_COLOR_PICKER_CUSTOM_CODE = """
+<table width="100%" cellspacing="5"><tbody><tr><td>
+    <label for="r_{name}">Red:</label>
+</td><td width="80%">
+    <input id="r_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
+</td><td rowspan="3" style="padding-left: 10px;">
+    <div id="preview_{name}"
+      style="line-height: 40px; margin-bottom: 7px; width: 100%; background-color: {value};">&nbsp;</div>
+    <input class="form-control" type="text" maxlength="7" id="{name}" name="{name}" value="{value}"
+      onchange="v_{name}()" />
+</td></tr><tr><td>
+    <label for="g_{name}">Green:</label>
+</td><td>
+    <input id="g_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
+</td></tr><tr><td>
+    <label for="b_{name}">Blue:</label>
+</td><td>
+    <input id="b_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
+</td></tr></tbody></table>
+<script lang="javascript">
+    const hex_chars = "0123456789ABCDEF";
+    function i2hex(name) {
+        var i = document.getElementById(name).value;
+        return hex_chars.substr(parseInt(i / 16), 1) + hex_chars.substr(parseInt(i % 16), 1)
+    }
+    function u_{name}() {
+        document.getElementById("{name}").value = "#"+i2hex("r_{name}")+i2hex("g_{name}")+i2hex("b_{name}");
+        document.getElementById("preview_{name}").style.background = document.getElementById("{name}").value;
+        updateJSONconf();
+    }
+    function hex2i(text) {
+        return hex_chars.indexOf(text.substr(0,1)) * 16 + hex_chars.indexOf(text.substr(1,1));
+    }
+    function v_{name}() {
+        var value = document.getElementById("{name}").value.toUpperCase();
+        document.getElementById("r_{name}").value = hex2i(value.substr(1,2));
+        document.getElementById("g_{name}").value = hex2i(value.substr(3,2));
+        document.getElementById("b_{name}").value = hex2i(value.substr(5,2));
+        document.getElementById("preview_{name}").style.background = document.getElementById("{name}").value;
+    }
+    v_{name}();
+</script>
+"""
+
+with DAG(
+    dag_id=Path(__file__).stem,
+    description=__doc__[0 : __doc__.find(".")],
+    doc_md=__doc__,
+    schedule=None,
+    start_date=datetime.datetime(2022, 3, 4),
+    catchup=False,
+    tags=["example_ui"],
+    params={
+        # Let's start simple: Standard dict values are detected from type and offered as entry form fields.
+        # Detected types are numbers, text, boolean, lists and dicts.
+        # Note that such auto-detected parameters are treated as optional (not required to contain a value)
+        "x": 3,
+        "text": "Hello World!",
+        "flag": False,
+        "a_simple_list": ["one", "two", "three", "actually one value is made per line"],
+        # But of course you might want to have it nicer! Let's add some description to parameters.
+        # Note that you can add any arbitrary HTML formatting to the description as well.
+        "most_loved_number": Param(
+            42,
+            type="integer",
+            title="You favorite number",
+            description="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 "
+            "<a href='https://en.wikipedia.org/wiki/Phrases_from_The_Hitchhiker%27s_Guide_to_the_Galaxy#"
+            "The_Answer_to_the_Ultimate_Question_of_Life,_the_Universe,_and_Everything_is_42'>"
+            "The Hitchhiker's Guide to the Galaxy</a>",
+        ),
+        # If you want to have a selection list box then you can use the enum feature of JSON schema
+        "pick_one": Param(
+            "value 1",
+            type="string",
+            title="Select one Value",
+            description="You can use JSON schema enum's to generate drop down selection boxes.",
+            enum=[f"value {i}" for i in range(1, 42)],
+        ),
+        # Boolean as proper parameter with description
+        "bool": Param(
+            True,
+            type="boolean",
+            title="Please confirm",
+            description="A On/Off selection with a proper description.",
+        ),
+        # Dates and Times are also supported
+        "date_time": Param(
+            f"{datetime.date.today()} {datetime.time(hour=12, minute=17, second=00)}",
+            type="string",
+            format="date-time",
+            title="Date-Time Picker",
+            description="Please select a date and time, use the button on the left for a pup-up calendar.",
+        ),
+        "date": Param(
+            f"{datetime.date.today()}",
+            type="string",
+            format="date",
+            title="Date Picker",
+            description="Please select a date, use the button on the left for a pup-up calendar. "
+            "See that here are no times!",
+        ),
+        "time": Param(
+            f"{datetime.time(hour=12, minute=13, second=14)}",
+            type=["string", "null"],
+            format="time",
+            title="Time Picker",
+            description="Please select a time, use the button on the left for a pup-up tool.",
+        ),
+        # Fields can be required or not. If the defined fields are typed they are getting required by default
+        # (else they would not pass JSON schema validation) - to make typed fields optional you must
+        # permit the optional "null" type
+        "required_field": Param(
+            "You can not trigger if no text is given here!",
+            type="string",
+            title="Required text field",
+            description="This field is required. You can not submit without having text in here.",
+        ),
+        "optional_field": Param(
+            "optional text, you can trigger also w/o text",
+            type=["null", "string"],
+            title="Optional text field",
+            description="This field is optional. As field content is JSON schema validated you must allow "
+            "the <code>null</code> type.",
+        ),
+        # You can arrange the entry fields in sections so that you can have a better overview for the user
+        # Therefore you can add the "section" attribute.
+        # The benefit of the Params class definition is that the full scope of JSON schema validation
+        # can be leveraged for form fields and they will be validated before DAG submission.
+        "checked_text": Param(
+            "length-checked-field",
+            type="string",
+            title="Text field with length check",
+            description="This field is required. And you need to provide something between 10 and 30 "
+            "characters. See the "
+            "<a href='https://json-schema.org/understanding-json-schema/reference/string.html'>"
+            "JSON schema description (string)</a> in for more details",
+            minLength=10,
+            maxLength=20,
+            section="JSON Schema validation options",
+        ),
+        "checked_number": Param(
+            100,
+            type="number",
+            title="Number field with value check",
+            description="This field is required. You need to provide any number between 64 and 128. "
+            "See the <a href='https://json-schema.org/understanding-json-schema/reference/numeric.html'>"
+            "JSON schema description (numbers)</a> in for more details",
+            minimum=64,
+            maximum=128,
+            section="JSON Schema validation options",
+        ),
+        # Some further cool stuff as advanced options are also possible
+        # You can have the user entering a dict object as a JSON with validation
+        "object": Param(
+            {"key": "value"},
+            type=["object", "null"],
+            title="JSON entry field",
+            section="Special advanced stuff with form fields",
+        ),
+        # If you want to have static parameters which are always passed and not editable by the user
+        # then you can use the JSON schema option of passing constant values. These parameters
+        # will not be displayed but passed to the DAG
+        "hidden_secret_field": Param("constant value", const="constant value"),
+        # Finally besides the standard provided field generator you can have you own HTML form code
+        # injected - but be careful, you can also mess-up the layout!
+        "color_picker": Param(
+            "#FF8800",
+            type="string",
+            title="Pick a color",
+            description="This is a special HTML widget as custom implementation in the DAG code. "
+            "It is templated with the following parameter to render proper HTML form fields:"
+            "<ul>"
+            "    <li><code>{name}</code>: Name of the HTML input field that is expected.</li>"
+            "    <li><code>{value}</code>: (Default) value that should be displayed when showing/loading "
+            "        the form.</li>"
+            "    <li>Note: If you have elements changing a value, call <code>updateJSONconf()</code>.</li>"
+            "</ul>"
+            "Example: <code>&lt;input name='{name}' value='{value}' onchange='updateJSONconf()' /&gt;</code>",

Review Comment:
   Thanks for the review. That makes sense. I tried to bring the `markupsafe.Markup()` into the DAG/Params() section but it seems when the DAG and Params are serialized to DB the Markup class is lost and comes out as Python String object in the webserver after un-pickling :-( This the Markup-warpping is lost during serialization.
   
   So as alternative proposal I committed a small change such that we use an additional `description_html` field for cases where markup is needed. This better?
   
   If you also don't like this I assume other alternatives would be:
   2) Extending the `Params()` allowing to add the HTML markup needs there as property.
   3) Adjust the `Params()` serialization/deserialization to explicitly not lose Markup during pickling



##########
airflow/config_templates/config.yml:
##########
@@ -1529,6 +1529,19 @@ webserver:
       type: boolean
       example: ~
       default: "False"
+    trigger_dag_url:

Review Comment:
   This config entry belongs to part 2 of the AIP-50. As described in the PR header I implemented as proposal in a reduced manner to prevent an impact on DAG loading. The actual described feature in AIP-50 would require to load serialized DAG information into the overview page or an DB / model extension for actually a small feature and might have a performance impact w/o proper planning.
   
   I also wanted to use `url_for()` but this is only a possibility for internal routing. The idea in AIP-50 was that you can define a path to a plugin or other custom web front-end to re-direct the user to instead of the internal view.
   
   If you think this is "not good" I can revert this part and separate this out to another PR, I did not want to raise a discussion on this but wanted to close the gap to AIP-50 plans.



##########
airflow/www/views.py:
##########
@@ -1955,7 +1963,32 @@ def trigger(self, session=None):
         request_execution_date = request.values.get("execution_date", default=timezone.utcnow().isoformat())
         is_dag_run_conf_overrides_params = conf.getboolean("core", "dag_run_conf_overrides_params")
         dag = get_airflow_app().dag_bag.get_dag(dag_id)
-        dag_orm = session.query(DagModel).filter(DagModel.dag_id == dag_id).first()
+        dag_orm: DagModel = session.query(DagModel).filter(DagModel.dag_id == dag_id).first()
+
+        # Prepare form fields with param struct details to render a proper form with schema information
+        form_fields = {}
+        for k, v in dag.params.items():
+            form_fields[k] = v.dump()
+            # If no schema is provided, auto-detect on default values
+            if "schema" not in form_fields[k]:
+                form_fields[k]["schema"] = {}
+            if "type" not in form_fields[k]["schema"]:
+                if isinstance(form_fields[k]["value"], bool):
+                    form_fields[k]["schema"]["type"] = "boolean"
+                elif isinstance(form_fields[k]["value"], int):
+                    form_fields[k]["schema"]["type"] = ["integer", "null"]
+                elif isinstance(form_fields[k]["value"], list):
+                    form_fields[k]["schema"]["type"] = ["array", "null"]
+                elif isinstance(form_fields[k]["value"], dict):
+                    form_fields[k]["schema"]["type"] = ["object", "null"]
+            # Mark markup fields as safe
+            if "description" in form_fields[k] and form_fields[k]["description"]:
+                form_fields[k]["description"] = Markup(form_fields[k]["description"])

Review Comment:
   See comment above, changed to Markup() only being used for `description_html`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] pierrejeambrun commented on pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "pierrejeambrun (via GitHub)" <gi...@apache.org>.
pierrejeambrun commented on PR #27063:
URL: https://github.com/apache/airflow/pull/27063#issuecomment-1488910094

   - Main is a 'development' branch, not stable
   - There are in progress changes not finalized that you do not want in your production env
   - Most importantly, there are potentially breaking changes marked for `Airflow 3.0.0` that could break your install.
   
   I would say that you could cherry pick only the changes related to AIP-50 and apply that to your own version (if you really can't wait). This is a new feature, so it will be part of the next minor release. `2.6.0` is just around the corner as it will most probably be the next released version of airflow. (right after `2.5.3`)
   
   I would recommend to wait just a little bit longer.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] pierrejeambrun commented on pull request #27063: [WIP/Preview] Feature/26215 Proposal for AIP-50 Trigger UI based on FAB

Posted by GitBox <gi...@apache.org>.
pierrejeambrun commented on PR #27063:
URL: https://github.com/apache/airflow/pull/27063#issuecomment-1330513543

   @jens-scheffler-bosch 
   I could find time to try this yet. I will take a look this week, thanks for this preview :)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] jens-scheffler-bosch commented on pull request #27063: [WIP/Preview] Feature/26215 Proposal for AIP-50 Trigger UI based on FAB

Posted by GitBox <gi...@apache.org>.
jens-scheffler-bosch commented on PR #27063:
URL: https://github.com/apache/airflow/pull/27063#issuecomment-1279501136

   Note that Pytests are not adjusted and are even failing for the WIP PR at the moment, will be fixed before the PR leaves the WIP phase. Still the function is working and can be inspected.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] bbovenzi commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "bbovenzi (via GitHub)" <gi...@apache.org>.
bbovenzi commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1084252178


##########
airflow/www/static/js/trigger.js:
##########
@@ -19,33 +19,190 @@
 
 /* global document, CodeMirror, window */
 
-const textArea = document.getElementById('json');
+let jsonForm;
+const objectFields = new Map();
 const recentConfigList = document.getElementById('recent_configs');
-const minHeight = 300;
-const maxHeight = window.innerHeight - 450;
-const height = maxHeight > minHeight ? maxHeight : minHeight;
-
-CodeMirror.fromTextArea(textArea, {
-  lineNumbers: true,
-  mode: {
-    name: 'javascript',
-    json: true,
-  },
-  gutters: ['CodeMirror-lint-markers'],
-  lint: true,
-})
-  .setSize(null, height);
+
+/**
+ * Update the generated JSON DagRun.conf JSON field if any field changed
+ */
+function updateJSONconf() {
+  const jsonStart = document.getElementById('json_start').value;
+  const params = JSON.parse(jsonStart);
+  const elements = document.getElementById('trigger_form');
+  for (let i = 0; i < elements.length; i += 1) {
+    if (elements[i].name && elements[i].name.startsWith('element_')) {
+      const keyName = elements[i].name.substr(8);
+      if (elements[i].type === 'checkbox') {
+        params[keyName] = elements[i].checked;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'array') {
+        const lines = elements[i].value.split('\n');
+        const values = [];
+        for (let j = 0; j < lines.length; j += 1) {
+          if (lines[j].trim().length > 0) {
+            values[values.length] = lines[j].trim();
+          }
+        }
+        params[keyName] = values;
+      } else if (elements[i].value.length === 0) {
+        params[keyName] = null;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'object') {
+        try {
+          const textValue = objectFields.get(elements[i].name).getValue();
+          if (textValue.length > 0) {
+            const objValue = JSON.parse(textValue);
+            params[keyName] = objValue;
+            objectFields.get(elements[i].name).setValue(JSON.stringify(objValue, null, 4));
+          } else {
+            params[keyName] = null;
+          }
+        } catch (e) {
+          // ignore JSON parsing errors

Review Comment:
   Do we not show anything if there's an error with json parsing?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] jens-scheffler-bosch commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "jens-scheffler-bosch (via GitHub)" <gi...@apache.org>.
jens-scheffler-bosch commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1088321851


##########
airflow/config_templates/config.yml:
##########
@@ -1529,6 +1529,19 @@ webserver:
       type: boolean
       example: ~
       default: "False"
+    trigger_dag_url:

Review Comment:
   Reverted code for part 2, let's discuss separately. From Falsk documentation I did not understand how to use `url_for()` for external links but maybe I need to re-read.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] jens-scheffler-bosch commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "jens-scheffler-bosch (via GitHub)" <gi...@apache.org>.
jens-scheffler-bosch commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1088324192


##########
airflow/www/static/js/trigger.js:
##########
@@ -19,33 +19,190 @@
 
 /* global document, CodeMirror, window */
 
-const textArea = document.getElementById('json');
+let jsonForm;
+const objectFields = new Map();
 const recentConfigList = document.getElementById('recent_configs');
-const minHeight = 300;
-const maxHeight = window.innerHeight - 450;
-const height = maxHeight > minHeight ? maxHeight : minHeight;
-
-CodeMirror.fromTextArea(textArea, {
-  lineNumbers: true,
-  mode: {
-    name: 'javascript',
-    json: true,
-  },
-  gutters: ['CodeMirror-lint-markers'],
-  lint: true,
-})
-  .setSize(null, height);
+
+/**
+ * Update the generated JSON DagRun.conf JSON field if any field changed
+ */
+function updateJSONconf() {
+  const jsonStart = document.getElementById('json_start').value;
+  const params = JSON.parse(jsonStart);
+  const elements = document.getElementById('trigger_form');
+  for (let i = 0; i < elements.length; i += 1) {
+    if (elements[i].name && elements[i].name.startsWith('element_')) {
+      const keyName = elements[i].name.substr(8);
+      if (elements[i].type === 'checkbox') {
+        params[keyName] = elements[i].checked;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'array') {
+        const lines = elements[i].value.split('\n');
+        const values = [];
+        for (let j = 0; j < lines.length; j += 1) {
+          if (lines[j].trim().length > 0) {
+            values[values.length] = lines[j].trim();
+          }
+        }
+        params[keyName] = values;
+      } else if (elements[i].value.length === 0) {
+        params[keyName] = null;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'object') {
+        try {
+          const textValue = objectFields.get(elements[i].name).getValue();
+          if (textValue.length > 0) {
+            const objValue = JSON.parse(textValue);
+            params[keyName] = objValue;
+            objectFields.get(elements[i].name).setValue(JSON.stringify(objValue, null, 4));
+          } else {
+            params[keyName] = null;
+          }
+        } catch (e) {
+          // ignore JSON parsing errors
+        }
+      } else if (Number.isNaN(elements[i].value)) {
+        params[keyName] = elements[i].value;
+      } else if (elements[i].attributes.valuetype && elements[i].attributes.valuetype.value === 'number') {
+        params[keyName] = Number(elements[i].value);
+      } else {
+        params[keyName] = elements[i].value;
+      }
+    }
+  }
+  jsonForm.setValue(JSON.stringify(params, null, 4));
+}
+
+/**
+ * Initialize the form during load of the web page
+ */
+function initForm() {
+  const formSectionsElement = document.getElementById('form_sections');
+  const formWithFields = (formSectionsElement != null);
+
+  // Initialize the Generated JSON form or JSON entry form
+  const minHeight = 300;
+  const maxHeight = (formWithFields ? window.innerHeight / 2 : window.innerHeight) - 550;
+  const height = maxHeight > minHeight ? maxHeight : minHeight;
+  jsonForm = CodeMirror.fromTextArea(document.getElementById('json'), {
+    lineNumbers: true,
+    mode: { name: 'javascript', json: true },
+    gutters: ['CodeMirror-lint-markers'],
+    lint: true,
+  });
+  jsonForm.setSize(null, height);
+
+  if (formWithFields) {
+    // Apply JSON formatting and linting to all object fields in the form
+    const elements = document.getElementById('trigger_form');
+    for (let i = 0; i < elements.length; i += 1) {
+      if (elements[i].name && elements[i].name.startsWith('element_')) {
+        if (elements[i].attributes.valuetype
+            && elements[i].attributes.valuetype.value === 'object') {
+          const field = CodeMirror.fromTextArea(elements[i], {
+            lineNumbers: true,
+            mode: { name: 'javascript', json: true },
+            gutters: ['CodeMirror-lint-markers'],
+            lint: true,
+          });
+          /* eslint-disable no-unused-vars */
+          field.on('blur', (cm, change) => { updateJSONconf(); });

Review Comment:
   Great, thanks. I am not a JS expert, such via this PR I learned something new :-D



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] jens-scheffler-bosch commented on pull request #27063: [WIP/Preview] Feature/26215 Proposal for AIP-50 Trigger UI based on FAB

Posted by GitBox <gi...@apache.org>.
jens-scheffler-bosch commented on PR #27063:
URL: https://github.com/apache/airflow/pull/27063#issuecomment-1330545783

   > @jens-scheffler-bosch I couldn’t find time to try this yet. I will take a look this week, thanks for this preview :)
   
   Great, looing forward to complete the missing pieces if the direction/proposal is accepted!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] pierrejeambrun commented on pull request #27063: [WIP/Preview] Feature/26215 Proposal for AIP-50 Trigger UI based on FAB

Posted by GitBox <gi...@apache.org>.
pierrejeambrun commented on PR #27063:
URL: https://github.com/apache/airflow/pull/27063#issuecomment-1336185811

   @jens-scheffler-bosch Tested that locally and looks very cool. I think it is worth pushing that further with the addition you mentioned. (Doc, tests, cleaning the code etc.)
   
   I would mention that it would be great if this could integrate with the newly added `retrieval of recent config` feature https://github.com/apache/airflow/pull/27805.
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] ashb commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by GitBox <gi...@apache.org>.
ashb commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1066754342


##########
airflow/example_dags/example_params_ui_tutorial.py:
##########
@@ -0,0 +1,232 @@
+#
+# 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.
+"""DAG demonstrating various options for a trigger form generated by DAG params.
+
+The DAG attribute `params` is used to define a default dictionary of parameters which are usually passed
+to the DAG and which are used to render a trigger form.
+"""
+from __future__ import annotations
+
+import datetime
+import json
+from pathlib import Path
+
+from airflow import DAG
+from airflow.decorators import task
+from airflow.exceptions import AirflowSkipException
+from airflow.models.dagrun import DagRun
+from airflow.models.param import Param
+from airflow.models.taskinstance import TaskInstance
+
+HTML_COLOR_PICKER_CUSTOM_CODE = """
+<table width="100%" cellspacing="5"><tbody><tr><td>
+    <label for="r_{name}">Red:</label>
+</td><td width="80%">
+    <input id="r_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
+</td><td rowspan="3" style="padding-left: 10px;">
+    <div id="preview_{name}"
+      style="line-height: 40px; margin-bottom: 7px; width: 100%; background-color: {value};">&nbsp;</div>
+    <input class="form-control" type="text" maxlength="7" id="{name}" name="{name}" value="{value}"
+      onchange="v_{name}()" />
+</td></tr><tr><td>
+    <label for="g_{name}">Green:</label>
+</td><td>
+    <input id="g_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
+</td></tr><tr><td>
+    <label for="b_{name}">Blue:</label>
+</td><td>
+    <input id="b_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
+</td></tr></tbody></table>
+<script lang="javascript">
+    const hex_chars = "0123456789ABCDEF";
+    function i2hex(name) {
+        var i = document.getElementById(name).value;
+        return hex_chars.substr(parseInt(i / 16), 1) + hex_chars.substr(parseInt(i % 16), 1)
+    }
+    function u_{name}() {
+        document.getElementById("{name}").value = "#"+i2hex("r_{name}")+i2hex("g_{name}")+i2hex("b_{name}");
+        document.getElementById("preview_{name}").style.background = document.getElementById("{name}").value;
+        updateJSONconf();
+    }
+    function hex2i(text) {
+        return hex_chars.indexOf(text.substr(0,1)) * 16 + hex_chars.indexOf(text.substr(1,1));
+    }
+    function v_{name}() {
+        var value = document.getElementById("{name}").value.toUpperCase();
+        document.getElementById("r_{name}").value = hex2i(value.substr(1,2));
+        document.getElementById("g_{name}").value = hex2i(value.substr(3,2));
+        document.getElementById("b_{name}").value = hex2i(value.substr(5,2));
+        document.getElementById("preview_{name}").style.background = document.getElementById("{name}").value;
+    }
+    v_{name}();
+</script>
+"""
+
+with DAG(
+    dag_id=Path(__file__).stem,
+    description=__doc__[0 : __doc__.find(".")],
+    doc_md=__doc__,
+    schedule=None,
+    start_date=datetime.datetime(2022, 3, 4),
+    catchup=False,
+    tags=["example_ui"],
+    params={
+        # Let's start simple: Standard dict values are detected from type and offered as entry form fields.
+        # Detected types are numbers, text, boolean, lists and dicts.
+        # Note that such auto-detected parameters are treated as optional (not required to contain a value)
+        "x": 3,
+        "text": "Hello World!",
+        "flag": False,
+        "a_simple_list": ["one", "two", "three", "actually one value is made per line"],
+        # But of course you might want to have it nicer! Let's add some description to parameters.
+        # Note that you can add any arbitrary HTML formatting to the description as well.
+        "most_loved_number": Param(
+            42,
+            type="integer",
+            title="You favorite number",
+            description="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 "
+            "<a href='https://en.wikipedia.org/wiki/Phrases_from_The_Hitchhiker%27s_Guide_to_the_Galaxy#"
+            "The_Answer_to_the_Ultimate_Question_of_Life,_the_Universe,_and_Everything_is_42'>"
+            "The Hitchhiker's Guide to the Galaxy</a>",
+        ),
+        # If you want to have a selection list box then you can use the enum feature of JSON schema
+        "pick_one": Param(
+            "value 1",
+            type="string",
+            title="Select one Value",
+            description="You can use JSON schema enum's to generate drop down selection boxes.",
+            enum=[f"value {i}" for i in range(1, 42)],
+        ),
+        # Boolean as proper parameter with description
+        "bool": Param(
+            True,
+            type="boolean",
+            title="Please confirm",
+            description="A On/Off selection with a proper description.",
+        ),
+        # Dates and Times are also supported
+        "date_time": Param(
+            f"{datetime.date.today()} {datetime.time(hour=12, minute=17, second=00)}",
+            type="string",
+            format="date-time",
+            title="Date-Time Picker",
+            description="Please select a date and time, use the button on the left for a pup-up calendar.",
+        ),
+        "date": Param(
+            f"{datetime.date.today()}",
+            type="string",
+            format="date",
+            title="Date Picker",
+            description="Please select a date, use the button on the left for a pup-up calendar. "
+            "See that here are no times!",
+        ),
+        "time": Param(
+            f"{datetime.time(hour=12, minute=13, second=14)}",
+            type=["string", "null"],
+            format="time",
+            title="Time Picker",
+            description="Please select a time, use the button on the left for a pup-up tool.",
+        ),
+        # Fields can be required or not. If the defined fields are typed they are getting required by default
+        # (else they would not pass JSON schema validation) - to make typed fields optional you must
+        # permit the optional "null" type
+        "required_field": Param(
+            "You can not trigger if no text is given here!",
+            type="string",
+            title="Required text field",
+            description="This field is required. You can not submit without having text in here.",
+        ),
+        "optional_field": Param(
+            "optional text, you can trigger also w/o text",
+            type=["null", "string"],
+            title="Optional text field",
+            description="This field is optional. As field content is JSON schema validated you must allow "
+            "the <code>null</code> type.",
+        ),
+        # You can arrange the entry fields in sections so that you can have a better overview for the user
+        # Therefore you can add the "section" attribute.
+        # The benefit of the Params class definition is that the full scope of JSON schema validation
+        # can be leveraged for form fields and they will be validated before DAG submission.
+        "checked_text": Param(
+            "length-checked-field",
+            type="string",
+            title="Text field with length check",
+            description="This field is required. And you need to provide something between 10 and 30 "
+            "characters. See the "
+            "<a href='https://json-schema.org/understanding-json-schema/reference/string.html'>"
+            "JSON schema description (string)</a> in for more details",
+            minLength=10,
+            maxLength=20,
+            section="JSON Schema validation options",
+        ),
+        "checked_number": Param(
+            100,
+            type="number",
+            title="Number field with value check",
+            description="This field is required. You need to provide any number between 64 and 128. "
+            "See the <a href='https://json-schema.org/understanding-json-schema/reference/numeric.html'>"
+            "JSON schema description (numbers)</a> in for more details",
+            minimum=64,
+            maximum=128,
+            section="JSON Schema validation options",
+        ),
+        # Some further cool stuff as advanced options are also possible
+        # You can have the user entering a dict object as a JSON with validation
+        "object": Param(
+            {"key": "value"},
+            type=["object", "null"],
+            title="JSON entry field",
+            section="Special advanced stuff with form fields",
+        ),
+        # If you want to have static parameters which are always passed and not editable by the user
+        # then you can use the JSON schema option of passing constant values. These parameters
+        # will not be displayed but passed to the DAG
+        "hidden_secret_field": Param("constant value", const="constant value"),
+        # Finally besides the standard provided field generator you can have you own HTML form code
+        # injected - but be careful, you can also mess-up the layout!
+        "color_picker": Param(
+            "#FF8800",
+            type="string",
+            title="Pick a color",
+            description="This is a special HTML widget as custom implementation in the DAG code. "
+            "It is templated with the following parameter to render proper HTML form fields:"
+            "<ul>"
+            "    <li><code>{name}</code>: Name of the HTML input field that is expected.</li>"
+            "    <li><code>{value}</code>: (Default) value that should be displayed when showing/loading "
+            "        the form.</li>"
+            "    <li>Note: If you have elements changing a value, call <code>updateJSONconf()</code>.</li>"
+            "</ul>"
+            "Example: <code>&lt;input name='{name}' value='{value}' onchange='updateJSONconf()' /&gt;</code>",

Review Comment:
   When this is rendered it should probably go through an HTML filter, so this should be be wrapped in `markupsafe.Markup()` here.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] bbovenzi merged pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "bbovenzi (via GitHub)" <gi...@apache.org>.
bbovenzi merged PR #27063:
URL: https://github.com/apache/airflow/pull/27063


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] jens-scheffler-bosch commented on pull request #27063: Feature/26215 AIP-50 Trigger UI based on FAB

Posted by GitBox <gi...@apache.org>.
jens-scheffler-bosch commented on PR #27063:
URL: https://github.com/apache/airflow/pull/27063#issuecomment-1363689224

   Hi @pierrejeambrun - thanks again for the review! I now closed the gaps and made the pipeline green - functional-wise the AIP-50 implementation is completed now. Now time for a final review, then (in my view) ready to be merged.
   
   Recent commits changed:
   - Added part 2 of the AIP-50 implementation
   - Added documentation
   - Fixed unit tests and needed some commits to make pipeline green


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] ashb commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "ashb (via GitHub)" <gi...@apache.org>.
ashb commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1083798621


##########
airflow/example_dags/example_params_ui_tutorial.py:
##########
@@ -0,0 +1,234 @@
+#
+# 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.
+"""DAG demonstrating various options for a trigger form generated by DAG params.
+
+The DAG attribute `params` is used to define a default dictionary of parameters which are usually passed
+to the DAG and which are used to render a trigger form.
+"""
+from __future__ import annotations
+
+import datetime
+import json
+from pathlib import Path
+
+from airflow import DAG
+from airflow.decorators import task
+from airflow.exceptions import AirflowSkipException
+from airflow.models.dagrun import DagRun
+from airflow.models.param import Param
+from airflow.models.taskinstance import TaskInstance
+
+HTML_COLOR_PICKER_CUSTOM_CODE = """
+<table width="100%" cellspacing="5"><tbody><tr><td>
+    <label for="r_{name}">Red:</label>
+</td><td width="80%">
+    <input id="r_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
+</td><td rowspan="3" style="padding-left: 10px;">
+    <div id="preview_{name}"
+      style="line-height: 40px; margin-bottom: 7px; width: 100%; background-color: {value};">&nbsp;</div>
+    <input class="form-control" type="text" maxlength="7" id="{name}" name="{name}" value="{value}"
+      onchange="v_{name}()" />
+</td></tr><tr><td>
+    <label for="g_{name}">Green:</label>
+</td><td>
+    <input id="g_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
+</td></tr><tr><td>
+    <label for="b_{name}">Blue:</label>
+</td><td>
+    <input id="b_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
+</td></tr></tbody></table>
+<script lang="javascript">
+    const hex_chars = "0123456789ABCDEF";
+    function i2hex(name) {
+        var i = document.getElementById(name).value;
+        return hex_chars.substr(parseInt(i / 16), 1) + hex_chars.substr(parseInt(i % 16), 1)
+    }
+    function u_{name}() {
+        document.getElementById("{name}").value = "#"+i2hex("r_{name}")+i2hex("g_{name}")+i2hex("b_{name}");
+        document.getElementById("preview_{name}").style.background = document.getElementById("{name}").value;
+        updateJSONconf();
+    }
+    function hex2i(text) {
+        return hex_chars.indexOf(text.substr(0,1)) * 16 + hex_chars.indexOf(text.substr(1,1));
+    }
+    function v_{name}() {
+        var value = document.getElementById("{name}").value.toUpperCase();
+        document.getElementById("r_{name}").value = hex2i(value.substr(1,2));
+        document.getElementById("g_{name}").value = hex2i(value.substr(3,2));
+        document.getElementById("b_{name}").value = hex2i(value.substr(5,2));
+        document.getElementById("preview_{name}").style.background = document.getElementById("{name}").value;
+    }
+    v_{name}();
+</script>
+"""
+
+with DAG(
+    dag_id=Path(__file__).stem,
+    description=__doc__[0 : __doc__.find(".")],
+    doc_md=__doc__,
+    schedule=None,
+    start_date=datetime.datetime(2022, 3, 4),
+    catchup=False,
+    tags=["example_ui"],
+    params={
+        # Let's start simple: Standard dict values are detected from type and offered as entry form fields.
+        # Detected types are numbers, text, boolean, lists and dicts.
+        # Note that such auto-detected parameters are treated as optional (not required to contain a value)
+        "x": 3,
+        "text": "Hello World!",
+        "flag": False,
+        "a_simple_list": ["one", "two", "three", "actually one value is made per line"],
+        # But of course you might want to have it nicer! Let's add some description to parameters.
+        # Note if you can add any HTML formatting to the description, you need to use the description_html
+        # attribute.
+        "most_loved_number": Param(
+            42,
+            type="integer",
+            title="You 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
+            <a href='https://en.wikipedia.org/wiki/Phrases_from_The_Hitchhiker%27s_Guide_to_the_Galaxy#
+            The_Answer_to_the_Ultimate_Question_of_Life,_the_Universe,_and_Everything_is_42'>
+            The Hitchhiker's Guide to the Galaxy</a>""",
+        ),
+        # If you want to have a selection list box then you can use the enum feature of JSON schema
+        "pick_one": Param(
+            "value 1",
+            type="string",
+            title="Select one Value",
+            description="You can use JSON schema enum's to generate drop down selection boxes.",
+            enum=[f"value {i}" for i in range(1, 42)],
+        ),
+        # Boolean as proper parameter with description
+        "bool": Param(
+            True,
+            type="boolean",
+            title="Please confirm",
+            description="A On/Off selection with a proper description.",
+        ),
+        # Dates and Times are also supported
+        "date_time": Param(
+            f"{datetime.date.today()} {datetime.time(hour=12, minute=17, second=00)}",
+            type="string",
+            format="date-time",
+            title="Date-Time Picker",
+            description="Please select a date and time, use the button on the left for a pup-up calendar.",
+        ),
+        "date": Param(
+            f"{datetime.date.today()}",
+            type="string",
+            format="date",
+            title="Date Picker",
+            description="Please select a date, use the button on the left for a pup-up calendar. "
+            "See that here are no times!",
+        ),
+        "time": Param(
+            f"{datetime.time(hour=12, minute=13, second=14)}",
+            type=["string", "null"],
+            format="time",
+            title="Time Picker",
+            description="Please select a time, use the button on the left for a pup-up tool.",
+        ),
+        # Fields can be required or not. If the defined fields are typed they are getting required by default
+        # (else they would not pass JSON schema validation) - to make typed fields optional you must
+        # permit the optional "null" type
+        "required_field": Param(
+            "You can not trigger if no text is given here!",
+            type="string",
+            title="Required text field",
+            description="This field is required. You can not submit without having text in here.",
+        ),
+        "optional_field": Param(
+            "optional text, you can trigger also w/o text",
+            type=["null", "string"],
+            title="Optional text field",
+            description_html="This field is optional. As field content is JSON schema validated you must "
+            "allow the <code>null</code> type.",
+        ),
+        # You can arrange the entry fields in sections so that you can have a better overview for the user
+        # Therefore you can add the "section" attribute.
+        # The benefit of the Params class definition is that the full scope of JSON schema validation
+        # can be leveraged for form fields and they will be validated before DAG submission.
+        "checked_text": Param(
+            "length-checked-field",
+            type="string",
+            title="Text field with length check",
+            description_html="""This field is required. And you need to provide something between 10 and 30
+            characters. See the
+            <a href='https://json-schema.org/understanding-json-schema/reference/string.html'>
+            JSON schema description (string)</a> in for more details""",
+            minLength=10,
+            maxLength=20,
+            section="JSON Schema validation options",
+        ),
+        "checked_number": Param(
+            100,
+            type="number",
+            title="Number field with value check",
+            description_html="""This field is required. You need to provide any number between 64 and 128.
+            See the <a href='https://json-schema.org/understanding-json-schema/reference/numeric.html'>
+            JSON schema description (numbers)</a> in for more details""",
+            minimum=64,
+            maximum=128,
+            section="JSON Schema validation options",
+        ),
+        # Some further cool stuff as advanced options are also possible
+        # You can have the user entering a dict object as a JSON with validation
+        "object": Param(
+            {"key": "value"},
+            type=["object", "null"],
+            title="JSON entry field",
+            section="Special advanced stuff with form fields",
+        ),
+        # If you want to have static parameters which are always passed and not editable by the user
+        # then you can use the JSON schema option of passing constant values. These parameters
+        # will not be displayed but passed to the DAG
+        "hidden_secret_field": Param("constant value", const="constant value"),
+        # Finally besides the standard provided field generator you can have you own HTML form code
+        # injected - but be careful, you can also mess-up the layout!
+        "color_picker": Param(
+            "#FF8800",
+            type="string",
+            title="Pick a color",
+            description_html="""This is a special HTML widget as custom implementation in the DAG code.
+            It is templated with the following parameter to render proper HTML form fields:
+            <ul>
+                <li><code>{name}</code>: Name of the HTML input field that is expected.</li>
+                <li><code>{value}</code>:
+                    (Default) value that should be displayed when showing/loading the form.</li>
+                <li>Note: If you have elements changing a value, call <code>updateJSONconf()</code>.</li>
+            </ul>
+            Example: <code>&lt;input name='{name}' value='{value}' onchange='updateJSONconf()' /&gt;</code>

Review Comment:
   Rather than needing custom form's to use `onchange='updateJSONconf()'` can we change the even to pick up all inputs under the form automatically?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] jens-scheffler-bosch commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "jens-scheffler-bosch (via GitHub)" <gi...@apache.org>.
jens-scheffler-bosch commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1088323111


##########
airflow/example_dags/example_params_ui_tutorial.py:
##########
@@ -0,0 +1,234 @@
+#
+# 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.
+"""DAG demonstrating various options for a trigger form generated by DAG params.
+
+The DAG attribute `params` is used to define a default dictionary of parameters which are usually passed
+to the DAG and which are used to render a trigger form.
+"""
+from __future__ import annotations
+
+import datetime
+import json
+from pathlib import Path
+
+from airflow import DAG
+from airflow.decorators import task
+from airflow.exceptions import AirflowSkipException
+from airflow.models.dagrun import DagRun
+from airflow.models.param import Param
+from airflow.models.taskinstance import TaskInstance
+
+HTML_COLOR_PICKER_CUSTOM_CODE = """
+<table width="100%" cellspacing="5"><tbody><tr><td>
+    <label for="r_{name}">Red:</label>
+</td><td width="80%">
+    <input id="r_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
+</td><td rowspan="3" style="padding-left: 10px;">
+    <div id="preview_{name}"
+      style="line-height: 40px; margin-bottom: 7px; width: 100%; background-color: {value};">&nbsp;</div>
+    <input class="form-control" type="text" maxlength="7" id="{name}" name="{name}" value="{value}"
+      onchange="v_{name}()" />
+</td></tr><tr><td>
+    <label for="g_{name}">Green:</label>
+</td><td>
+    <input id="g_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
+</td></tr><tr><td>
+    <label for="b_{name}">Blue:</label>
+</td><td>
+    <input id="b_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
+</td></tr></tbody></table>
+<script lang="javascript">
+    const hex_chars = "0123456789ABCDEF";
+    function i2hex(name) {
+        var i = document.getElementById(name).value;
+        return hex_chars.substr(parseInt(i / 16), 1) + hex_chars.substr(parseInt(i % 16), 1)
+    }
+    function u_{name}() {
+        document.getElementById("{name}").value = "#"+i2hex("r_{name}")+i2hex("g_{name}")+i2hex("b_{name}");
+        document.getElementById("preview_{name}").style.background = document.getElementById("{name}").value;
+        updateJSONconf();
+    }
+    function hex2i(text) {
+        return hex_chars.indexOf(text.substr(0,1)) * 16 + hex_chars.indexOf(text.substr(1,1));
+    }
+    function v_{name}() {
+        var value = document.getElementById("{name}").value.toUpperCase();
+        document.getElementById("r_{name}").value = hex2i(value.substr(1,2));
+        document.getElementById("g_{name}").value = hex2i(value.substr(3,2));
+        document.getElementById("b_{name}").value = hex2i(value.substr(5,2));
+        document.getElementById("preview_{name}").style.background = document.getElementById("{name}").value;
+    }
+    v_{name}();
+</script>
+"""
+
+with DAG(
+    dag_id=Path(__file__).stem,
+    description=__doc__[0 : __doc__.find(".")],
+    doc_md=__doc__,
+    schedule=None,
+    start_date=datetime.datetime(2022, 3, 4),
+    catchup=False,
+    tags=["example_ui"],
+    params={
+        # Let's start simple: Standard dict values are detected from type and offered as entry form fields.
+        # Detected types are numbers, text, boolean, lists and dicts.
+        # Note that such auto-detected parameters are treated as optional (not required to contain a value)
+        "x": 3,
+        "text": "Hello World!",
+        "flag": False,
+        "a_simple_list": ["one", "two", "three", "actually one value is made per line"],
+        # But of course you might want to have it nicer! Let's add some description to parameters.
+        # Note if you can add any HTML formatting to the description, you need to use the description_html
+        # attribute.
+        "most_loved_number": Param(
+            42,
+            type="integer",
+            title="You 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
+            <a href='https://en.wikipedia.org/wiki/Phrases_from_The_Hitchhiker%27s_Guide_to_the_Galaxy#
+            The_Answer_to_the_Ultimate_Question_of_Life,_the_Universe,_and_Everything_is_42'>
+            The Hitchhiker's Guide to the Galaxy</a>""",
+        ),
+        # If you want to have a selection list box then you can use the enum feature of JSON schema
+        "pick_one": Param(
+            "value 1",
+            type="string",
+            title="Select one Value",
+            description="You can use JSON schema enum's to generate drop down selection boxes.",
+            enum=[f"value {i}" for i in range(1, 42)],
+        ),
+        # Boolean as proper parameter with description
+        "bool": Param(
+            True,
+            type="boolean",
+            title="Please confirm",
+            description="A On/Off selection with a proper description.",
+        ),
+        # Dates and Times are also supported
+        "date_time": Param(
+            f"{datetime.date.today()} {datetime.time(hour=12, minute=17, second=00)}",
+            type="string",
+            format="date-time",
+            title="Date-Time Picker",
+            description="Please select a date and time, use the button on the left for a pup-up calendar.",
+        ),
+        "date": Param(
+            f"{datetime.date.today()}",
+            type="string",
+            format="date",
+            title="Date Picker",
+            description="Please select a date, use the button on the left for a pup-up calendar. "
+            "See that here are no times!",
+        ),
+        "time": Param(
+            f"{datetime.time(hour=12, minute=13, second=14)}",
+            type=["string", "null"],
+            format="time",
+            title="Time Picker",
+            description="Please select a time, use the button on the left for a pup-up tool.",
+        ),
+        # Fields can be required or not. If the defined fields are typed they are getting required by default
+        # (else they would not pass JSON schema validation) - to make typed fields optional you must
+        # permit the optional "null" type
+        "required_field": Param(
+            "You can not trigger if no text is given here!",
+            type="string",
+            title="Required text field",
+            description="This field is required. You can not submit without having text in here.",
+        ),
+        "optional_field": Param(
+            "optional text, you can trigger also w/o text",
+            type=["null", "string"],
+            title="Optional text field",
+            description_html="This field is optional. As field content is JSON schema validated you must "
+            "allow the <code>null</code> type.",
+        ),
+        # You can arrange the entry fields in sections so that you can have a better overview for the user
+        # Therefore you can add the "section" attribute.
+        # The benefit of the Params class definition is that the full scope of JSON schema validation
+        # can be leveraged for form fields and they will be validated before DAG submission.
+        "checked_text": Param(
+            "length-checked-field",
+            type="string",
+            title="Text field with length check",
+            description_html="""This field is required. And you need to provide something between 10 and 30
+            characters. See the
+            <a href='https://json-schema.org/understanding-json-schema/reference/string.html'>
+            JSON schema description (string)</a> in for more details""",
+            minLength=10,
+            maxLength=20,
+            section="JSON Schema validation options",
+        ),
+        "checked_number": Param(
+            100,
+            type="number",
+            title="Number field with value check",
+            description_html="""This field is required. You need to provide any number between 64 and 128.
+            See the <a href='https://json-schema.org/understanding-json-schema/reference/numeric.html'>
+            JSON schema description (numbers)</a> in for more details""",
+            minimum=64,
+            maximum=128,
+            section="JSON Schema validation options",
+        ),
+        # Some further cool stuff as advanced options are also possible
+        # You can have the user entering a dict object as a JSON with validation
+        "object": Param(
+            {"key": "value"},
+            type=["object", "null"],
+            title="JSON entry field",
+            section="Special advanced stuff with form fields",
+        ),
+        # If you want to have static parameters which are always passed and not editable by the user
+        # then you can use the JSON schema option of passing constant values. These parameters
+        # will not be displayed but passed to the DAG
+        "hidden_secret_field": Param("constant value", const="constant value"),
+        # Finally besides the standard provided field generator you can have you own HTML form code
+        # injected - but be careful, you can also mess-up the layout!
+        "color_picker": Param(
+            "#FF8800",
+            type="string",
+            title="Pick a color",
+            description_html="""This is a special HTML widget as custom implementation in the DAG code.
+            It is templated with the following parameter to render proper HTML form fields:
+            <ul>
+                <li><code>{name}</code>: Name of the HTML input field that is expected.</li>
+                <li><code>{value}</code>:
+                    (Default) value that should be displayed when showing/loading the form.</li>
+                <li>Note: If you have elements changing a value, call <code>updateJSONconf()</code>.</li>
+            </ul>
+            Example: <code>&lt;input name='{name}' value='{value}' onchange='updateJSONconf()' /&gt;</code>

Review Comment:
   I tried to apply this comment but when looking to my own example I realized that a "generic" solution might be hard. I fear this generates more problems than it helps users, because adding the event listeners globally might generate side effects when a custom form has other event listeners and the order is not right.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] jens-scheffler-bosch commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "jens-scheffler-bosch (via GitHub)" <gi...@apache.org>.
jens-scheffler-bosch commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1088325402


##########
airflow/www/templates/airflow/trigger.html:
##########
@@ -27,11 +27,93 @@
   <link rel="stylesheet" type="text/css" href="{{ url_for_asset('switch.css') }}">
 {% endblock %}
 
+{% macro form_element(form_key, form_details) %}
+<tr>
+  <td class="col-lg-2">
+    <label for="element_{{ form_key }}" control-label="">
+      {%- if "title" in form_details.schema and form_details.schema.title -%}
+        {{ form_details.schema.title }}
+      {%- else -%}
+        {{ form_key }}
+      {%- endif -%}
+      {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type and not "boolean" in form_details.schema.type -%}
+        <strong style="color: red"> *</strong>
+      {%- endif -%}
+    : </label>
+  </td>
+  <td>
+    {% if "custom_html_form" in form_details.schema %}
+      {{ form_details.schema.custom_html_form | replace("{name}", "element_" + form_key) | replace("{value}", form_details.value) }}
+    {% elif "type" in form_details.schema and form_details.schema.type == "boolean" %}
+      <label for="element_{{ form_key }}" control-label="">
+        <input class="switch-input" name="element_{{ form_key }}" id="element_{{ form_key }}" type="checkbox"
+          {%- if form_details.value %} checked="checked"{%- endif -%}/>
+        <span class="switch" aria-hidden="true"></span>
+      </label>
+    {% elif "format" in form_details.schema and "date-time" in form_details.schema.format %}
+      <div class="input-group datetime datetimepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "date" in form_details.schema.format %}
+      <div class="input-group datetime datepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %} value="{% if form_details.value %}{{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "format" in form_details.schema and "time" in form_details.schema.format %}
+      <div class="input-group datetime timepicker">
+        <span class="input-group-addon"><span class="material-icons cursor-hand">calendar_today</span></span>
+        <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" type="text" valuetype="date"
+          {%- if not "null" in form_details.schema.type %} required=""{%- endif %}
+          value="{% if form_details.value %}{{ form_details.value[0:2] }}00-01-01 {{ form_details.value }}{% endif %}" />
+      </div>
+    {% elif "enum" in form_details.schema and form_details.schema.enum %}
+      <select class="my_select2 form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" data-placeholder="Select Value"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}" onchange="updateJSONconf();"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif %}>
+        {% for option in form_details.schema.enum -%}
+        <option>{{ option }}</option>
+        {% endfor -%}
+      </select>
+    {% elif form_details.schema and "array" in form_details.schema.type %}
+      <textarea class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" valuetype="array" rows="6"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif -%}>
+        {%- for txt in form_details.value -%}
+          {{ txt }}{{ "\n" }}
+        {%- endfor -%}
+      </textarea>
+    {% elif form_details.schema and "object" in form_details.schema.type %}
+      <textarea class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" valuetype="object" rows="6"
+        {%- if not "null" in form_details.schema.type %} required=""{% endif -%}>
+        {{- form_details.value | tojson() -}}
+      </textarea>
+    {% elif form_details.schema and ("integer" in form_details.schema.type or "number" in form_details.schema.type) %}
+      <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" placeholder="" valuetype="number" type="number"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}"
+        {%- if form_details.schema.minimum %} min="{{ form_details.schema.minimum }}"{% endif %}
+        {%- if form_details.schema.maximum %} max="{{ form_details.schema.maximum }}"{% endif %}
+        {%- if form_details.schema.type and not "null" in form_details.schema.type %} required=""{% endif %} />
+    {% else %}
+      <input class="form-control" name="element_{{ form_key }}" id="element_{{ form_key }}" placeholder="" type="text"
+        value="{% if form_details.value %}{{ form_details.value }}{% endif %}"
+        {%- if form_details.schema and form_details.schema.minLength %} minlength="{{ form_details.schema.minLength }}"{% endif %}
+        {%- if form_details.schema and form_details.schema.maxLength %} maxlength="{{ form_details.schema.maxLength }}"{% endif %}
+        {%- if form_details.schema and form_details.schema.type and not "null" in form_details.schema.type %} required=""{% endif %} />
+    {% endif %}
+    {% if form_details.description -%}

Review Comment:
   I'll add some screenshots to the code lines, hope this is not bloating the readability. But makes review easier probably. Otherwise spin up the web server and call the example_params_ui_tutorial DAG, this contains examples for all features.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] github-actions[bot] commented on pull request #27063: [WIP/Preview] Feature/26215 Proposal for AIP-50 Trigger UI based on FAB

Posted by GitBox <gi...@apache.org>.
github-actions[bot] commented on PR #27063:
URL: https://github.com/apache/airflow/pull/27063#issuecomment-1329908972

   This pull request has been automatically marked as stale because it has not had recent activity. It will be closed in 5 days if no further activity occurs. Thank you for your contributions.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] ashb commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "ashb (via GitHub)" <gi...@apache.org>.
ashb commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1083793113


##########
airflow/config_templates/config.yml:
##########
@@ -1529,6 +1529,19 @@ webserver:
       type: boolean
       example: ~
       default: "False"
+    trigger_dag_url:

Review Comment:
   Yeah, if you could separate this that would be good.
   
   (One option for when re-doing it _might_ be store the route name in config, i.e. the value of the first argument to `url_for`?)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] jens-scheffler-bosch commented on pull request #27063: AIP-50 Trigger UI based on FAB

Posted by "jens-scheffler-bosch (via GitHub)" <gi...@apache.org>.
jens-scheffler-bosch commented on PR #27063:
URL: https://github.com/apache/airflow/pull/27063#issuecomment-1405665838

   Hi @ashb && @bbovenzi applied feedback, resolved merge conflict, added screen snippets, reverted part 2 of AIP-50 - ready for next stage of review!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] ashb commented on a diff in pull request #27063: AIP-50 Trigger UI based on FAB

Posted by GitBox <gi...@apache.org>.
ashb commented on code in PR #27063:
URL: https://github.com/apache/airflow/pull/27063#discussion_r1066752407


##########
airflow/config_templates/config.yml:
##########
@@ -1529,6 +1529,19 @@ webserver:
       type: boolean
       example: ~
       default: "False"
+    trigger_dag_url:

Review Comment:
   Why do we need this btw? (As in when would someone ever set it?)
   
   I'm wary about hard-coding URLs like this in the  config, as this would need extra config if someone sets the webserver root to something other than `/` -- i.e. why can't we use `{{ url_for(..., dag_id=...) }}` in the template>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [airflow] pierrejeambrun commented on pull request #27063: [WIP/Preview] Feature/26215 Proposal for AIP-50 Trigger UI based on FAB

Posted by GitBox <gi...@apache.org>.
pierrejeambrun commented on PR #27063:
URL: https://github.com/apache/airflow/pull/27063#issuecomment-1336191334

   To address your questions, (imho):
   - What you mentioned is I believe a good start to have something mergeable. (test, doc, etc.) 
   - I believe this could make it in the next minor release -> 2.6.0
   - Adding this feature via FAB is perfectly fine for me. AIP-38 is a long term ongoing work. At the moment I am not aware of a change that would bring the `trigger` page into 'react world' so we might have this under FAB for quite a while before it gets updated.
   - Part 2: We could fetch these forms on the `trigger.html` page, not before (dag level). And ask the person to chose which form/format he wants to use via extra buttons/select. (just an idea to not have to get all forms for all dags on the home page).
   - I know there has been a few questions about this behavior (slack) and we might benefit from updating it. Today I see it like this, a dag 'can' always run without config, this is what happens when it is not manually triggered. Manually triggering the dag without config would mimic a 'scheduled' run in terms of config i.e not using params 'at all'. Wondering what others think about this. Maybe the empty dict is misleading and a plain 'None' would be more appropriate. We woudn't want scheduled run to use default params values.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org