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 <[email protected]>
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 };
};