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 }; };