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

jscheffl pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 6d16136b8fb Migrate trigger form params to new UI (#45270)
6d16136b8fb is described below

commit 6d16136b8fbed267c6dfa3de9004b8a1dc67fec4
Author: Jens Scheffler <95105677+jsche...@users.noreply.github.com>
AuthorDate: Thu Jan 16 16:51:34 2025 +0100

    Migrate trigger form params to new UI (#45270)
    
    * Add flexible form fields to trigger form WIP
    
    * Add flexible form fields to trigger form WIP
    
    * Fix dropdown issue
    
    * Add support for arrays of strings
    
    * Add support for advanced arrays
    
    * Remove placeholder for string
    
    * Add support for proposals in string fields
    
    * Add support for multi-select dropdowns
    
    * Add support for object fields
    
    * Add support for number fields
    
    * Add support for multiline text fields
    
    * Use other component for multi-select which displays the values
    
    * Implement support for form sections
    
    * Implement support for form sections
    
    * Add an alert that form ist not fully implemented
    
    * Review feedback - safe string handling
    
    * Review feedback, remove reloading warnings, move selector functions to 
separate TSX file
    
    * Review feedback - rename components for shorter name
    
    * Review feedback - adjust hard-coded color for boder in CodeMirror
    
    * Rework default field values
    
    * Move form elements into a common accorion
    
    * Review feedback
    
    * Review feedback, next round
    
    * Remove un-needed placeholders in date-time picker
---
 airflow/example_dags/example_params_ui_tutorial.py |   5 +-
 .../components/FlexibleForm/FieldAdvancedArray.tsx |  52 +++++++++
 .../ui/src/components/FlexibleForm/FieldBool.tsx   |  29 +++++
 .../src/components/FlexibleForm/FieldDateTime.tsx  |  31 ++++++
 .../src/components/FlexibleForm/FieldDropdown.tsx  |  66 ++++++++++++
 .../components/FlexibleForm/FieldMultiSelect.tsx   |  60 +++++++++++
 .../components/FlexibleForm/FieldMultilineText.tsx |  31 ++++++
 .../ui/src/components/FlexibleForm/FieldNumber.tsx |  34 ++++++
 .../ui/src/components/FlexibleForm/FieldObject.tsx |  52 +++++++++
 .../ui/src/components/FlexibleForm/FieldRow.tsx    |  49 +++++++++
 .../src/components/FlexibleForm/FieldSelector.tsx  | 118 +++++++++++++++++++++
 .../ui/src/components/FlexibleForm/FieldString.tsx |  45 ++++++++
 .../components/FlexibleForm/FieldStringArray.tsx   |  33 ++++++
 .../src/components/FlexibleForm/FlexibleForm.tsx   |  64 +++++++++++
 .../ui/src/components/FlexibleForm/HiddenInput.tsx |  28 +++++
 airflow/ui/src/components/FlexibleForm/Row.tsx     |  29 +++++
 airflow/ui/src/components/FlexibleForm/index.tsx   |  32 ++++++
 .../src/components/TriggerDag/TriggerDAGForm.tsx   |  16 ++-
 airflow/ui/src/components/ui/NumberInput.tsx       |  40 +++++++
 airflow/ui/src/queries/useDagParams.ts             |  41 +++++--
 20 files changed, 843 insertions(+), 12 deletions(-)

diff --git a/airflow/example_dags/example_params_ui_tutorial.py 
b/airflow/example_dags/example_params_ui_tutorial.py
index 5d21e6b774d..b64e777bed1 100644
--- a/airflow/example_dags/example_params_ui_tutorial.py
+++ b/airflow/example_dags/example_params_ui_tutorial.py
@@ -94,7 +94,7 @@ with (
             # If you want to have a list box with proposals but not enforcing 
a fixed list
             # then you can use the examples feature of JSON schema
             "proposals": Param(
-                "some value",
+                "Alpha",
                 type="string",
                 title="Field with proposals",
                 description="You can use JSON schema examples's to generate 
drop down selection boxes "
@@ -169,6 +169,7 @@ with (
             "multiline_text": Param(
                 "A multiline text Param\nthat will keep the 
newline\ncharacters in its value.",
                 description="This field allows for multiline text input. The 
returned value will be a single with newline (\\n) characters kept intact.",
+                title="Multiline text",
                 type=["string", "null"],
                 format="multiline",
             ),
@@ -205,7 +206,7 @@ with (
             [schema description 
(string)](https://json-schema.org/understanding-json-schema/reference/string.html)
             for more details""",
                 minLength=10,
-                maxLength=20,
+                maxLength=30,
                 section="JSON Schema validation options",
             ),
             "checked_number": Param(
diff --git a/airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx 
b/airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx
new file mode 100644
index 00000000000..ac995c8ca13
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx
@@ -0,0 +1,52 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { json } from "@codemirror/lang-json";
+import { githubLight, githubDark } from "@uiw/codemirror-themes-all";
+import CodeMirror from "@uiw/react-codemirror";
+
+import { useColorMode } from "src/context/colorMode";
+
+import type { FlexibleFormElementProps } from ".";
+
+export const FieldAdvancedArray = ({ name, param }: FlexibleFormElementProps) 
=> {
+  const { colorMode } = useColorMode();
+
+  return (
+    <CodeMirror
+      basicSetup={{
+        autocompletion: true,
+        bracketMatching: true,
+        foldGutter: true,
+        lineNumbers: true,
+      }}
+      extensions={[json()]}
+      height="200px"
+      id={`element_${name}`}
+      style={{
+        border: "1px solid var(--chakra-colors-border)",
+        borderRadius: "8px",
+        outline: "none",
+        padding: "2px",
+        width: "100%",
+      }}
+      theme={colorMode === "dark" ? githubDark : githubLight}
+      value={JSON.stringify(param.value ?? [], undefined, 2)}
+    />
+  );
+};
diff --git a/airflow/ui/src/components/FlexibleForm/FieldBool.tsx 
b/airflow/ui/src/components/FlexibleForm/FieldBool.tsx
new file mode 100644
index 00000000000..609e7ade5a1
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/FieldBool.tsx
@@ -0,0 +1,29 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { FlexibleFormElementProps } from ".";
+import { Switch } from "../ui";
+
+export const FieldBool = ({ name, param }: FlexibleFormElementProps) => (
+  <Switch
+    colorPalette="blue"
+    defaultChecked={Boolean(param.value)}
+    id={`element_${name}`}
+    name={`element_${name}`}
+  />
+);
diff --git a/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx 
b/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx
new file mode 100644
index 00000000000..f109363988a
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx
@@ -0,0 +1,31 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Input, type InputProps } from "@chakra-ui/react";
+
+import type { FlexibleFormElementProps } from ".";
+
+export const FieldDateTime = ({ name, param, ...rest }: 
FlexibleFormElementProps & InputProps) => (
+  <Input
+    defaultValue={typeof param.value === "string" ? param.value : undefined}
+    id={`element_${name}`}
+    name={`element_${name}`}
+    size="sm"
+    type={rest.type}
+  />
+);
diff --git a/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx 
b/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx
new file mode 100644
index 00000000000..bbea023168b
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx
@@ -0,0 +1,66 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { createListCollection } from "@chakra-ui/react/collection";
+import { useRef } from "react";
+
+import { Select } from "src/components/ui";
+
+import type { FlexibleFormElementProps } from ".";
+
+const labelLookup = (key: string, valuesDisplay: Record<string, string> | 
undefined): string => {
+  if (valuesDisplay && typeof valuesDisplay === "object") {
+    return valuesDisplay[key] ?? key;
+  }
+
+  return key;
+};
+const enumTypes = ["string", "number", "integer"];
+
+export const FieldDropdown = ({ name, param }: FlexibleFormElementProps) => {
+  const selectOptions = createListCollection({
+    items:
+      param.schema.enum?.map((value) => ({
+        label: labelLookup(value, param.schema.values_display),
+        value,
+      })) ?? [],
+  });
+  const contentRef = useRef<HTMLDivElement>(null);
+
+  return (
+    <Select.Root
+      collection={selectOptions}
+      defaultValue={enumTypes.includes(typeof param.value) ? 
[String(param.value)] : undefined}
+      id={`element_${name}`}
+      name={`element_${name}`}
+      ref={contentRef}
+      size="sm"
+    >
+      <Select.Trigger>
+        <Select.ValueText placeholder="Select Value" />
+      </Select.Trigger>
+      <Select.Content portalRef={contentRef}>
+        {selectOptions.items.map((option) => (
+          <Select.Item item={option} key={option.value}>
+            {option.label}
+          </Select.Item>
+        ))}
+      </Select.Content>
+    </Select.Root>
+  );
+};
diff --git a/airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx 
b/airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx
new file mode 100644
index 00000000000..02ae473324c
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx
@@ -0,0 +1,60 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Select as ReactSelect } from "chakra-react-select";
+import { useState } from "react";
+
+import type { FlexibleFormElementProps } from ".";
+
+const labelLookup = (key: string, valuesDisplay: Record<string, string> | 
undefined): string => {
+  if (valuesDisplay && typeof valuesDisplay === "object") {
+    return valuesDisplay[key] ?? key;
+  }
+
+  return key;
+};
+
+export const FieldMultiSelect = ({ name, param }: FlexibleFormElementProps) => 
{
+  const [selectedOptions, setSelectedOptions] = useState(
+    Array.isArray(param.value)
+      ? (param.value as Array<string>).map((value) => ({
+          label: labelLookup(value, param.schema.values_display),
+          value,
+        }))
+      : [],
+  );
+
+  return (
+    <ReactSelect
+      aria-label="Select one or multiple values"
+      id={`element_${name}`}
+      isClearable
+      isMulti
+      name={`element_${name}`}
+      onChange={(newValue) => setSelectedOptions([...newValue])}
+      options={
+        param.schema.examples?.map((value) => ({
+          label: labelLookup(value, param.schema.values_display),
+          value,
+        })) ?? []
+      }
+      size="sm"
+      value={selectedOptions}
+    />
+  );
+};
diff --git a/airflow/ui/src/components/FlexibleForm/FieldMultilineText.tsx 
b/airflow/ui/src/components/FlexibleForm/FieldMultilineText.tsx
new file mode 100644
index 00000000000..70ee631cd3c
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/FieldMultilineText.tsx
@@ -0,0 +1,31 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Textarea } from "@chakra-ui/react";
+
+import type { FlexibleFormElementProps } from ".";
+
+export const FieldMultilineText = ({ name, param }: FlexibleFormElementProps) 
=> (
+  <Textarea
+    defaultValue={String(param.value ?? "")}
+    id={`element_${name}`}
+    name={`element_${name}`}
+    rows={6}
+    size="sm"
+  />
+);
diff --git a/airflow/ui/src/components/FlexibleForm/FieldNumber.tsx 
b/airflow/ui/src/components/FlexibleForm/FieldNumber.tsx
new file mode 100644
index 00000000000..710b884c776
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/FieldNumber.tsx
@@ -0,0 +1,34 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { FlexibleFormElementProps } from ".";
+import { NumberInputField, NumberInputRoot } from "../ui/NumberInput";
+
+export const FieldNumber = ({ name, param }: FlexibleFormElementProps) => (
+  <NumberInputRoot
+    allowMouseWheel
+    defaultValue={String(param.value ?? "")}
+    id={`element_${name}`}
+    max={param.schema.maximum ?? undefined}
+    min={param.schema.minimum ?? undefined}
+    name={`element_${name}`}
+    size="sm"
+  >
+    <NumberInputField />
+  </NumberInputRoot>
+);
diff --git a/airflow/ui/src/components/FlexibleForm/FieldObject.tsx 
b/airflow/ui/src/components/FlexibleForm/FieldObject.tsx
new file mode 100644
index 00000000000..e019eb6607f
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/FieldObject.tsx
@@ -0,0 +1,52 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { json } from "@codemirror/lang-json";
+import { githubLight, githubDark } from "@uiw/codemirror-themes-all";
+import CodeMirror from "@uiw/react-codemirror";
+
+import { useColorMode } from "src/context/colorMode";
+
+import type { FlexibleFormElementProps } from ".";
+
+export const FieldObject = ({ name, param }: FlexibleFormElementProps) => {
+  const { colorMode } = useColorMode();
+
+  return (
+    <CodeMirror
+      basicSetup={{
+        autocompletion: true,
+        bracketMatching: true,
+        foldGutter: true,
+        lineNumbers: true,
+      }}
+      extensions={[json()]}
+      height="200px"
+      id={`element_${name}`}
+      style={{
+        border: "1px solid var(--chakra-colors-border)",
+        borderRadius: "8px",
+        outline: "none",
+        padding: "2px",
+        width: "100%",
+      }}
+      theme={colorMode === "dark" ? githubDark : githubLight}
+      value={JSON.stringify(param.value ?? {}, undefined, 2)}
+    />
+  );
+};
diff --git a/airflow/ui/src/components/FlexibleForm/FieldRow.tsx 
b/airflow/ui/src/components/FlexibleForm/FieldRow.tsx
new file mode 100644
index 00000000000..583c8c62b54
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/FieldRow.tsx
@@ -0,0 +1,49 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Field, Stack } from "@chakra-ui/react";
+import Markdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+
+import type { ParamSpec } from "src/queries/useDagParams";
+
+import type { FlexibleFormElementProps } from ".";
+import { FieldSelector } from "./FieldSelector";
+
+const isRequired = (param: ParamSpec) =>
+  // The field is required if the schema type is defined.
+  // But if the type "null" is included, then the field is not required.
+  // We assume that "null" is only defined if the type is an array.
+  Boolean(param.schema.type) && (!Array.isArray(param.schema.type) || 
!param.schema.type.includes("null"));
+
+/** Render a normal form row with a field that is auto-selected */
+export const FieldRow = ({ name, param }: FlexibleFormElementProps) => (
+  <Field.Root orientation="horizontal" required={isRequired(param)}>
+    <Stack>
+      <Field.Label fontSize="md">
+        {param.schema.title ?? name} <Field.RequiredIndicator />
+      </Field.Label>
+    </Stack>
+    <Stack css={{ "flex-basis": "70%" }}>
+      <FieldSelector name={name} param={param} />
+      <Field.HelperText>
+        {param.description ?? <Markdown 
remarkPlugins={[remarkGfm]}>{param.schema.description_md}</Markdown>}
+      </Field.HelperText>
+    </Stack>
+  </Field.Root>
+);
diff --git a/airflow/ui/src/components/FlexibleForm/FieldSelector.tsx 
b/airflow/ui/src/components/FlexibleForm/FieldSelector.tsx
new file mode 100644
index 00000000000..ddedb75728b
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/FieldSelector.tsx
@@ -0,0 +1,118 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { ParamSchema, ParamSpec } from "src/queries/useDagParams";
+
+import type { FlexibleFormElementProps } from ".";
+import { FieldAdvancedArray } from "./FieldAdvancedArray";
+import { FieldBool } from "./FieldBool";
+import { FieldDateTime } from "./FieldDateTime";
+import { FieldDropdown } from "./FieldDropdown";
+import { FieldMultiSelect } from "./FieldMultiSelect";
+import { FieldMultilineText } from "./FieldMultilineText";
+import { FieldNumber } from "./FieldNumber";
+import { FieldObject } from "./FieldObject";
+import { FieldString } from "./FieldString";
+import { FieldStringArray } from "./FieldStringArray";
+
+const inferType = (param: ParamSpec) => {
+  if (Boolean(param.schema.type)) {
+    // If there are multiple types, we assume that the first one is the 
correct one that is not "null".
+    // "null" is only used to signal the value is optional.
+    if (Array.isArray(param.schema.type)) {
+      return param.schema.type.find((type) => type !== "null") ?? "string";
+    }
+
+    return param.schema.type ?? "string";
+  }
+
+  // If the type is not defined, we infer it from the value.
+  if (Array.isArray(param.value)) {
+    return "array";
+  }
+
+  return typeof param.value;
+};
+
+const isFieldAdvancedArray = (fieldType: string, fieldSchema: ParamSchema) =>
+  fieldType === "array" && fieldSchema.items?.type !== "string";
+
+const isFieldBool = (fieldType: string) => fieldType === "boolean";
+
+const isFieldDate = (fieldType: string, fieldSchema: ParamSchema) =>
+  fieldType === "string" && fieldSchema.format === "date";
+
+const isFieldDateTime = (fieldType: string, fieldSchema: ParamSchema) =>
+  fieldType === "string" && fieldSchema.format === "date-time";
+
+const enumTypes = ["string", "number", "integer"];
+
+const isFieldDropdown = (fieldType: string, fieldSchema: ParamSchema) =>
+  enumTypes.includes(fieldType) && Array.isArray(fieldSchema.enum);
+
+const isFieldMultilineText = (fieldType: string, fieldSchema: ParamSchema) =>
+  fieldType === "string" && fieldSchema.format === "multiline";
+
+const isFieldMultiSelect = (fieldType: string, fieldSchema: ParamSchema) =>
+  fieldType === "array" && Array.isArray(fieldSchema.examples);
+
+const isFieldNumber = (fieldType: string) => {
+  const numberTypes = ["integer", "number"];
+
+  return numberTypes.includes(fieldType);
+};
+
+const isFieldObject = (fieldType: string) => fieldType === "object";
+
+const isFieldStringArray = (fieldType: string, fieldSchema: ParamSchema) =>
+  fieldType === "array" &&
+  (!fieldSchema.items || fieldSchema.items.type === undefined || 
fieldSchema.items.type === "string");
+
+const isFieldTime = (fieldType: string, fieldSchema: ParamSchema) =>
+  fieldType === "string" && fieldSchema.format === "time";
+
+export const FieldSelector = ({ name, param }: FlexibleFormElementProps) => {
+  // FUTURE: Add support for other types as described in AIP-68 via Plugins
+  const fieldType = inferType(param);
+
+  if (isFieldBool(fieldType)) {
+    return <FieldBool name={name} param={param} />;
+  } else if (isFieldDateTime(fieldType, param.schema)) {
+    return <FieldDateTime name={name} param={param} type="datetime-local" />;
+  } else if (isFieldDate(fieldType, param.schema)) {
+    return <FieldDateTime name={name} param={param} type="date" />;
+  } else if (isFieldTime(fieldType, param.schema)) {
+    return <FieldDateTime name={name} param={param} type="time" />;
+  } else if (isFieldDropdown(fieldType, param.schema)) {
+    return <FieldDropdown name={name} param={param} />;
+  } else if (isFieldMultiSelect(fieldType, param.schema)) {
+    return <FieldMultiSelect name={name} param={param} />;
+  } else if (isFieldStringArray(fieldType, param.schema)) {
+    return <FieldStringArray name={name} param={param} />;
+  } else if (isFieldAdvancedArray(fieldType, param.schema)) {
+    return <FieldAdvancedArray name={name} param={param} />;
+  } else if (isFieldObject(fieldType)) {
+    return <FieldObject name={name} param={param} />;
+  } else if (isFieldNumber(fieldType)) {
+    return <FieldNumber name={name} param={param} />;
+  } else if (isFieldMultilineText(fieldType, param.schema)) {
+    return <FieldMultilineText name={name} param={param} />;
+  } else {
+    return <FieldString name={name} param={param} />;
+  }
+};
diff --git a/airflow/ui/src/components/FlexibleForm/FieldString.tsx 
b/airflow/ui/src/components/FlexibleForm/FieldString.tsx
new file mode 100644
index 00000000000..a3901801621
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/FieldString.tsx
@@ -0,0 +1,45 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Input } from "@chakra-ui/react";
+
+import type { FlexibleFormElementProps } from ".";
+
+export const FieldString = ({ name, param }: FlexibleFormElementProps) => (
+  <>
+    <Input
+      defaultValue={String(param.value ?? "")}
+      id={`element_${name}`}
+      list={param.schema.examples ? `list_${name}` : undefined}
+      maxLength={param.schema.maxLength ?? undefined}
+      minLength={param.schema.minLength ?? undefined}
+      name={`element_${name}`}
+      placeholder={param.schema.examples ? "Start typing to see options." : 
undefined}
+      size="sm"
+    />
+    {param.schema.examples ? (
+      <datalist id={`list_${name}`}>
+        {param.schema.examples.map((example) => (
+          <option key={example} value={example}>
+            {example}
+          </option>
+        ))}
+      </datalist>
+    ) : undefined}
+  </>
+);
diff --git a/airflow/ui/src/components/FlexibleForm/FieldStringArray.tsx 
b/airflow/ui/src/components/FlexibleForm/FieldStringArray.tsx
new file mode 100644
index 00000000000..494cd2e59fd
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/FieldStringArray.tsx
@@ -0,0 +1,33 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Textarea } from "@chakra-ui/react";
+
+import type { FlexibleFormElementProps } from ".";
+
+export const FieldStringArray = ({ name, param }: FlexibleFormElementProps) => 
(
+  <Textarea
+    defaultValue={
+      Array.isArray(param.value) ? (param.value as Array<string>).join("\n") : 
String(param.value ?? "")
+    }
+    id={`element_${name}`}
+    name={`element_${name}`}
+    rows={6}
+    size="sm"
+  />
+);
diff --git a/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx 
b/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
new file mode 100644
index 00000000000..17070037b6a
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
@@ -0,0 +1,64 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Stack, StackSeparator } from "@chakra-ui/react";
+
+import { flexibleFormDefaultSection, type FlexibleFormProps } from ".";
+import { Accordion, Alert } from "../ui";
+import { Row } from "./Row";
+
+export const FlexibleForm = ({ params }: FlexibleFormProps) => {
+  const processedSections = new Map();
+
+  return Object.entries(params).some(([, param]) => typeof 
param.schema.section !== "string")
+    ? Object.entries(params).map(([, secParam]) => {
+        const currentSection = secParam.schema.section ?? 
flexibleFormDefaultSection;
+
+        if (processedSections.has(currentSection)) {
+          return undefined;
+        } else {
+          processedSections.set(currentSection, true);
+
+          return (
+            <Accordion.Item key={currentSection} value={currentSection}>
+              <Accordion.ItemTrigger 
cursor="button">{currentSection}</Accordion.ItemTrigger>
+              <Accordion.ItemContent>
+                <Stack separator={<StackSeparator />}>
+                  <Alert
+                    status="warning"
+                    title="Population of changes in trigger form fields is not 
implemented yet. Please stay tuned for upcoming updates... and change the run 
conf in the 'Advanced Options' conf section below meanwhile."
+                  />
+                  {Object.entries(params)
+                    .filter(
+                      ([, param]) =>
+                        param.schema.section === currentSection ||
+                        (currentSection === flexibleFormDefaultSection && 
!Boolean(param.schema.section)),
+                    )
+                    .map(([name, param]) => (
+                      <Row key={name} name={name} param={param} />
+                    ))}
+                </Stack>
+              </Accordion.ItemContent>
+            </Accordion.Item>
+          );
+        }
+      })
+    : undefined;
+};
+
+export default FlexibleForm;
diff --git a/airflow/ui/src/components/FlexibleForm/HiddenInput.tsx 
b/airflow/ui/src/components/FlexibleForm/HiddenInput.tsx
new file mode 100644
index 00000000000..718a679b30f
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/HiddenInput.tsx
@@ -0,0 +1,28 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { VisuallyHidden } from "@chakra-ui/react";
+
+import type { FlexibleFormElementProps } from ".";
+
+/** Render a "const" field where user can not change data as hidden */
+export const HiddenInput = ({ name, param }: FlexibleFormElementProps) => (
+  <VisuallyHidden asChild>
+    <input id={`element_${name}`} name={`element_${name}`} type="hidden" 
value={String(param.value ?? "")} />
+  </VisuallyHidden>
+);
diff --git a/airflow/ui/src/components/FlexibleForm/Row.tsx 
b/airflow/ui/src/components/FlexibleForm/Row.tsx
new file mode 100644
index 00000000000..615d9400455
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/Row.tsx
@@ -0,0 +1,29 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { ParamSchema } from "src/queries/useDagParams";
+
+import type { FlexibleFormElementProps } from ".";
+import { FieldRow } from "./FieldRow";
+import { HiddenInput } from "./HiddenInput";
+
+const isHidden = (fieldSchema: ParamSchema) => Boolean(fieldSchema.const);
+
+/** Generates a form row */
+export const Row = ({ name, param }: FlexibleFormElementProps) =>
+  isHidden(param.schema) ? <HiddenInput name={name} param={param} /> : 
<FieldRow name={name} param={param} />;
diff --git a/airflow/ui/src/components/FlexibleForm/index.tsx 
b/airflow/ui/src/components/FlexibleForm/index.tsx
new file mode 100644
index 00000000000..8c8d6818a90
--- /dev/null
+++ b/airflow/ui/src/components/FlexibleForm/index.tsx
@@ -0,0 +1,32 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { DagParamsSpec, ParamSpec } from "src/queries/useDagParams";
+
+export type FlexibleFormProps = {
+  readonly params: DagParamsSpec;
+};
+
+export type FlexibleFormElementProps = {
+  readonly name: string;
+  readonly param: ParamSpec;
+};
+
+export const flexibleFormDefaultSection = "Run Parameters";
+
+export { FlexibleForm } from "./FlexibleForm";
diff --git a/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx 
b/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
index ecd2cb3f98a..6048a27f5a0 100644
--- a/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
+++ b/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
@@ -29,6 +29,7 @@ import { useDagParams } from "src/queries/useDagParams";
 import { useTrigger } from "src/queries/useTrigger";
 
 import { ErrorAlert } from "../ErrorAlert";
+import { FlexibleForm, flexibleFormDefaultSection } from "../FlexibleForm";
 import { Accordion } from "../ui";
 
 type TriggerDAGFormProps = {
@@ -47,13 +48,14 @@ export type DagRunTriggerParams = {
 
 const TriggerDAGForm = ({ dagId, onClose, open }: TriggerDAGFormProps) => {
   const [errors, setErrors] = useState<{ conf?: string; date?: unknown }>({});
-  const conf = useDagParams(dagId, open);
+  const { initialConf, paramsDict } = useDagParams(dagId, open);
   const {
     dateValidationError,
     error: errorTrigger,
     isPending,
     triggerDagRun,
   } = useTrigger({ onSuccessConfirm: onClose });
+  const conf = initialConf;
 
   const {
     control,
@@ -129,7 +131,15 @@ const TriggerDAGForm = ({ dagId, onClose, open }: 
TriggerDAGFormProps) => {
 
   return (
     <>
-      <Accordion.Root collapsible mb={4} mt={4} size="lg" variant="enclosed">
+      <Accordion.Root
+        collapsible
+        defaultValue={[flexibleFormDefaultSection]}
+        mb={4}
+        mt={4}
+        size="lg"
+        variant="enclosed"
+      >
+        <FlexibleForm params={paramsDict} />
         <Accordion.Item key="advancedOptions" value="advancedOptions">
           <Accordion.ItemTrigger cursor="button">Advanced 
Options</Accordion.ItemTrigger>
           <Accordion.ItemContent>
@@ -205,7 +215,7 @@ const TriggerDAGForm = ({ dagId, onClose, open }: 
TriggerDAGFormProps) => {
                         field.onChange(validateAndPrettifyJson(field.value));
                       }}
                       style={{
-                        border: "1px solid #CBD5E0",
+                        border: "1px solid var(--chakra-colors-border)",
                         borderRadius: "8px",
                         outline: "none",
                         padding: "2px",
diff --git a/airflow/ui/src/components/ui/NumberInput.tsx 
b/airflow/ui/src/components/ui/NumberInput.tsx
new file mode 100644
index 00000000000..3a6ed3ef48c
--- /dev/null
+++ b/airflow/ui/src/components/ui/NumberInput.tsx
@@ -0,0 +1,40 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { NumberInput as ChakraNumberInput } from "@chakra-ui/react";
+import * as React from "react";
+
+export type NumberInputProps = {} & ChakraNumberInput.RootProps;
+
+export const NumberInputRoot = React.forwardRef<HTMLDivElement, 
NumberInputProps>((props, ref) => {
+  const { children, ...rest } = props;
+
+  return (
+    <ChakraNumberInput.Root ref={ref} variant="outline" {...rest}>
+      {children}
+      <ChakraNumberInput.Control>
+        <ChakraNumberInput.IncrementTrigger />
+        <ChakraNumberInput.DecrementTrigger />
+      </ChakraNumberInput.Control>
+    </ChakraNumberInput.Root>
+  );
+});
+
+export const NumberInputField = ChakraNumberInput.Input;
+export const NumberInputScrubber = ChakraNumberInput.Scrubber;
+export const NumberInputLabel = ChakraNumberInput.Label;
diff --git a/airflow/ui/src/queries/useDagParams.ts 
b/airflow/ui/src/queries/useDagParams.ts
index 8bb0d53010c..2711099ac73 100644
--- a/airflow/ui/src/queries/useDagParams.ts
+++ b/airflow/ui/src/queries/useDagParams.ts
@@ -19,10 +19,37 @@
 import { useDagServiceGetDagDetails } from "openapi/queries";
 import { toaster } from "src/components/ui";
 
+export type DagParamsSpec = Record<string, ParamSpec>;
+
+export type ParamSpec = {
+  description: string | undefined;
+  schema: ParamSchema;
+  value: unknown;
+};
+
+export type ParamSchema = {
+  // TODO define the structure on API as generated code
+  const: string | undefined;
+  description_md: string | undefined;
+  enum: Array<string> | undefined;
+  examples: Array<string> | undefined;
+  format: string | undefined;
+  items: Record<string, unknown> | undefined;
+  maximum: number | undefined;
+  maxLength: number | undefined;
+  minimum: number | undefined;
+  minLength: number | undefined;
+  section: string | undefined;
+  title: string | undefined;
+  type: Array<string> | string | undefined;
+  values_display: Record<string, string> | undefined;
+};
+
 export const useDagParams = (dagId: string, open: boolean) => {
-  const { data, error } = useDagServiceGetDagDetails({ dagId }, undefined, {
-    enabled: open,
-  });
+  const { data, error }: { data?: Record<string, DagParamsSpec>; error?: 
unknown } =
+    useDagServiceGetDagDetails({ dagId }, undefined, {
+      enabled: open,
+    });
 
   if (Boolean(error)) {
     const errorDescription =
@@ -38,12 +65,12 @@ export const useDagParams = (dagId: string, open: boolean) 
=> {
   }
 
   const transformedParams = data?.params
-    ? Object.fromEntries(
-        Object.entries(data.params).map(([key, param]) => [key, (param as { 
value: unknown }).value]),
-      )
+    ? Object.fromEntries(Object.entries(data.params).map(([key, param]) => 
[key, param.value]))
     : {};
 
   const initialConf = JSON.stringify(transformedParams, undefined, 2);
 
-  return initialConf;
+  const paramsDict: DagParamsSpec = data?.params ?? ({} as DagParamsSpec);
+
+  return { initialConf, paramsDict };
 };


Reply via email to