bbovenzi commented on code in PR #45270: URL: https://github.com/apache/airflow/pull/45270#discussion_r1915097094
########## airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx: ########## @@ -205,7 +208,7 @@ const TriggerDAGForm = ({ dagId, onClose, open }: TriggerDAGFormProps) => { field.onChange(validateAndPrettifyJson(field.value)); }} style={{ - border: "1px solid #CBD5E0", + border: "1px solid var(--chakra-colors-border)", Review Comment: Oh, this is much better than what I suggested. ########## 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> | null): 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" Review Comment: ```suggestion aria-label="Select one or multiple values" ``` ########## airflow/ui/src/components/FlexibleForm/Row.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 type { ParamSchema } from "src/queries/useDagParams"; + +import type { FlexibleFormElementProps } from "."; +import { Hidden } from "./Hidden"; +import { NormalRow } from "./NormalRow"; + +const isHidden = (fieldSchema: ParamSchema) => Boolean(fieldSchema.const); + +/** Generates a form row */ +export const Row = ({ key, name, param }: FlexibleFormElementProps) => + isHidden(param.schema) ? ( + <Hidden key={key} name={name} param={param} /> + ) : ( + <NormalRow key={key} name={name} param={param} /> + ); Review Comment: ```suggestion export const Row = ({ name, param }: FlexibleFormElementProps) => isHidden(param.schema) ? ( <Hidden name={name} param={param} /> ) : ( <NormalRow name={name} param={param} /> ); ``` ########## airflow/ui/src/components/FlexibleForm/SelectElement.tsx: ########## @@ -0,0 +1,120 @@ +/*! + * 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 { FieldDate } from "./FieldDate"; +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"; +import { FieldTime } from "./FieldTime"; + +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 === "date"; + +export const SelectElement = ({ key, 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 key={key} name={name} param={param} />; + } else if (isFieldDateTime(fieldType, param.schema)) { + return <FieldDateTime key={key} name={name} param={param} />; + } else if (isFieldDate(fieldType, param.schema)) { + return <FieldDate key={key} name={name} param={param} />; + } else if (isFieldTime(fieldType, param.schema)) { + return <FieldTime key={key} name={name} param={param} />; + } else if (isFieldDropdown(fieldType, param.schema)) { Review Comment: We can consolidate Date, DateTime and Time components into one and just pass different values for `placeholder` and `type` ########## airflow/ui/src/components/FlexibleForm/SelectElement.tsx: ########## @@ -0,0 +1,120 @@ +/*! + * 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 { FieldDate } from "./FieldDate"; +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"; +import { FieldTime } from "./FieldTime"; + +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 === "date"; + +export const SelectElement = ({ key, name, param }: FlexibleFormElementProps) => { Review Comment: Let's rename this. I thought it was a dropdown select at first. Maybe `FieldInput`? ########## airflow/ui/src/queries/useDagParams.ts: ########## @@ -19,10 +19,36 @@ import { useDagServiceGetDagDetails } from "openapi/queries"; import { toaster } from "src/components/ui"; +export type DagParamsSpec = Record<string, ParamSpec>; + +export type ParamSpec = { + description: string | null; + schema: ParamSchema; + value: unknown; +}; + +export type ParamSchema = { + const: string | null; + description_md: string | null; + enum: Array<string> | null; + examples: Array<string> | null; + format: string | null; + items: Record<string, unknown> | null; + maximum: number | null; + maxLength: number | null; + minimum: number | null; + minLength: number | null; + section: string | null; + title: string | null; + type: Array<string> | string | null; + values_display: Record<string, string> | null; +}; Review Comment: Also, after looking at the actual dag params returned it looks like `undefined` might be a more accurate type than `null` ########## airflow/ui/src/queries/useDagParams.ts: ########## @@ -19,10 +19,36 @@ import { useDagServiceGetDagDetails } from "openapi/queries"; import { toaster } from "src/components/ui"; +export type DagParamsSpec = Record<string, ParamSpec>; + +export type ParamSpec = { + description: string | null; + schema: ParamSchema; + value: unknown; +}; + +export type ParamSchema = { + const: string | null; + description_md: string | null; + enum: Array<string> | null; + examples: Array<string> | null; + format: string | null; + items: Record<string, unknown> | null; + maximum: number | null; + maxLength: number | null; + minimum: number | null; + minLength: number | null; + section: string | null; + title: string | null; + type: Array<string> | string | null; + values_display: Record<string, string> | null; +}; Review Comment: We should update the API to already have these types instead of manually creating the types here. But I am fine if we clean that up in another PR. ########## airflow/ui/src/components/FlexibleForm/index.tsx: ########## @@ -0,0 +1,91 @@ +/*! + * 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 type { DagParamsSpec, ParamSpec } from "src/queries/useDagParams"; + +import { Accordion, Alert } from "../ui"; +import { Row } from "./Row"; + +type FlexibleFormProps = { + readonly params: DagParamsSpec; +}; + +export type FlexibleFormElementProps = { + readonly key: string; + readonly name: string; + readonly param: ParamSpec; +}; + +const FlexibleForm = ({ params }: FlexibleFormProps) => { + const processedSections = new Map(); + + return ( + <> + {Object.entries(params).some(([, param]) => typeof param.schema.section !== "string") ? ( + <Accordion.Item key="params" value="params"> + <Accordion.ItemTrigger cursor="button">Run Parameters</Accordion.ItemTrigger> + <Accordion.ItemContent> + <Stack separator={<StackSeparator />}> + {Object.keys(params).length > 0 && ( + <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." Review Comment: ```suggestion title="Work In Progress, use the 'Advanced Options' conf section below to actually edit dag run parameters for now." ``` ########## airflow/ui/src/components/FlexibleForm/index.tsx: ########## @@ -0,0 +1,91 @@ +/*! + * 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 type { DagParamsSpec, ParamSpec } from "src/queries/useDagParams"; + +import { Accordion, Alert } from "../ui"; +import { Row } from "./Row"; + +type FlexibleFormProps = { + readonly params: DagParamsSpec; +}; + +export type FlexibleFormElementProps = { + readonly key: string; Review Comment: ```suggestion ``` `key` is a reserved prop. We don't need to include it in our types. ########## airflow/ui/src/components/FlexibleForm/index.tsx: ########## Review Comment: In the new UI, `index` files should not contain any logic and simply export functions we want to use elsewhere. In this case, we should rename this file to `FlexibleForm.tsx` and then make a new `index.tsx` that exports the form and all the fields I probably should add a style guide to our contributing docs. ########## 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 proposal values." : undefined} Review Comment: ```suggestion placeholder={param.schema.examples ? "Start typing to see options." : undefined} ``` ########## airflow/ui/src/components/FlexibleForm/NormalRow.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 { SelectElement } from "./SelectElement"; + +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 NormalRow = ({ key, name, param }: FlexibleFormElementProps) => ( Review Comment: ```suggestion export const NormalRow = ({ name, param }: FlexibleFormElementProps) => ( ``` ########## airflow/ui/src/components/FlexibleForm/NormalRow.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 { SelectElement } from "./SelectElement"; + +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 NormalRow = ({ key, name, param }: FlexibleFormElementProps) => ( + <Field.Root orientation="horizontal" required={isRequired(param)}> + <Stack css={{ "flex-basis": "30%" }}> + <Field.Label css={{ "flex-basis": "0" }} fontSize="md"> + {param.schema.title ?? name} <Field.RequiredIndicator /> + </Field.Label> + </Stack> + <Stack css={{ "flex-basis": "70%" }}> Review Comment: ```suggestion <Stack flexBasis="30%"> <Field.Label flexBasis="0" fontSize="md"> {param.schema.title ?? name} <Field.RequiredIndicator /> </Field.Label> </Stack> <Stack flexBasis="70%"> ``` ########## airflow/ui/src/components/FlexibleForm/SelectElement.tsx: ########## @@ -0,0 +1,120 @@ +/*! + * 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 { FieldDate } from "./FieldDate"; +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"; +import { FieldTime } from "./FieldTime"; + +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 === "date"; + +export const SelectElement = ({ key, 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 key={key} name={name} param={param} />; Review Comment: We should remove all references to `key` in this file. It's not necessary ########## airflow/ui/src/components/FlexibleForm/Hidden.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 Hidden = ({ name, param }: FlexibleFormElementProps) => ( Review Comment: I think `HiddenInput` is more description ########## airflow/ui/src/components/FlexibleForm/index.tsx: ########## @@ -0,0 +1,91 @@ +/*! + * 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 type { DagParamsSpec, ParamSpec } from "src/queries/useDagParams"; + +import { Accordion, Alert } from "../ui"; +import { Row } from "./Row"; + +type FlexibleFormProps = { + readonly params: DagParamsSpec; +}; + +export type FlexibleFormElementProps = { + readonly key: string; + readonly name: string; + readonly param: ParamSpec; +}; + +const FlexibleForm = ({ params }: FlexibleFormProps) => { + const processedSections = new Map(); + + return ( + <> + {Object.entries(params).some(([, param]) => typeof param.schema.section !== "string") ? ( + <Accordion.Item key="params" value="params"> + <Accordion.ItemTrigger cursor="button">Run Parameters</Accordion.ItemTrigger> + <Accordion.ItemContent> + <Stack separator={<StackSeparator />}> + {Object.keys(params).length > 0 && ( + <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]) => typeof param.schema.section !== "string") + .map(([name, param]) => ( + <Row key={name} name={name} param={param} /> + ))} + </Stack> + </Accordion.ItemContent> + </Accordion.Item> + ) : undefined} + {Object.entries(params) + .filter(([, secParam]) => secParam.schema.section) + .map(([, secParam]) => { + const currentSection = secParam.schema.section; + + if (processedSections.has(currentSection)) { + return undefined; + } else { + processedSections.set(currentSection, true); + + return ( + <Accordion.Item key={secParam.schema.section ?? ""} value={secParam.schema.section ?? ""}> + <Accordion.ItemTrigger cursor="button">{secParam.schema.section}</Accordion.ItemTrigger> + <Accordion.ItemContent> + <Stack separator={<StackSeparator />}> + {Object.entries(params) + .filter(([, param]) => param.schema.section === currentSection) Review Comment: We can simplify this component a lot: ```suggestion {Object.entries(params) .map(([, secParam]) => { const currentSection = secParam.schema.section ?? "Run Parameters"; 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 />}> {Object.entries(params) .filter( ([, param]) => param.schema.section === currentSection || (currentSection === "Run Parameters" && !Boolean(param.schema.section)), ) ``` ########## airflow/ui/src/components/FlexibleForm/NormalRow.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 { SelectElement } from "./SelectElement"; + +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 NormalRow = ({ key, name, param }: FlexibleFormElementProps) => ( + <Field.Root orientation="horizontal" required={isRequired(param)}> + <Stack css={{ "flex-basis": "30%" }}> + <Field.Label css={{ "flex-basis": "0" }} fontSize="md"> + {param.schema.title ?? name} <Field.RequiredIndicator /> + </Field.Label> + </Stack> + <Stack css={{ "flex-basis": "70%" }}> + <SelectElement key={key} name={name} param={param} /> Review Comment: ```suggestion <SelectElement name={name} param={param} /> ``` ########## airflow/ui/src/components/FlexibleForm/SelectElement.tsx: ########## @@ -0,0 +1,120 @@ +/*! + * 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 { FieldDate } from "./FieldDate"; +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"; +import { FieldTime } from "./FieldTime"; + +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 === "date"; + +export const SelectElement = ({ key, 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 key={key} name={name} param={param} />; + } else if (isFieldDateTime(fieldType, param.schema)) { + return <FieldDateTime key={key} name={name} param={param} />; + } else if (isFieldDate(fieldType, param.schema)) { + return <FieldDate key={key} name={name} param={param} />; + } else if (isFieldTime(fieldType, param.schema)) { + return <FieldTime key={key} name={name} param={param} />; + } else if (isFieldDropdown(fieldType, param.schema)) { Review Comment: I think the same is true for StringArray and MultilineText. We only have to change the value we pass to it. ########## airflow/ui/src/components/FlexibleForm/NormalRow.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 { SelectElement } from "./SelectElement"; + +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 NormalRow = ({ key, name, param }: FlexibleFormElementProps) => ( Review Comment: I think something like `FlexibleField` is more descriptive than `NormalRow` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: commits-unsubscr...@airflow.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org