From 26853c307263b167ce201dcb8c2934eb14f6b698 Mon Sep 17 00:00:00 2001 From: Jens Scheffler Date: Sun, 29 Dec 2024 16:58:21 +0100 Subject: [PATCH] Add flexible form fields to trigger form WIP --- .../FlexibleForm/FlexibleFormFieldBool.tsx | 34 ++++++ .../FlexibleForm/FlexibleFormFieldDate.tsx | 38 ++++++ .../FlexibleFormFieldDateTime.tsx | 40 ++++++ .../FlexibleFormFieldDropdown.tsx | 78 ++++++++++++ .../FlexibleForm/FlexibleFormFieldString.tsx | 37 ++++++ .../FlexibleForm/FlexibleFormFieldTime.tsx | 38 ++++++ .../FlexibleForm/FlexibleFormHidden.tsx | 40 ++++++ .../FlexibleForm/FlexibleFormNormalRow.tsx | 115 ++++++++++++++++++ .../FlexibleForm/FlexibleFormRow.tsx | 29 +++++ .../ui/src/components/FlexibleForm/index.tsx | 43 +++++++ .../components/TriggerDag/TriggerDAGForm.tsx | 5 +- airflow/ui/src/queries/useDagParams.ts | 34 ++++-- 12 files changed, 522 insertions(+), 9 deletions(-) create mode 100644 airflow/ui/src/components/FlexibleForm/FlexibleFormFieldBool.tsx create mode 100644 airflow/ui/src/components/FlexibleForm/FlexibleFormFieldDate.tsx create mode 100644 airflow/ui/src/components/FlexibleForm/FlexibleFormFieldDateTime.tsx create mode 100644 airflow/ui/src/components/FlexibleForm/FlexibleFormFieldDropdown.tsx create mode 100644 airflow/ui/src/components/FlexibleForm/FlexibleFormFieldString.tsx create mode 100644 airflow/ui/src/components/FlexibleForm/FlexibleFormFieldTime.tsx create mode 100644 airflow/ui/src/components/FlexibleForm/FlexibleFormHidden.tsx create mode 100644 airflow/ui/src/components/FlexibleForm/FlexibleFormNormalRow.tsx create mode 100644 airflow/ui/src/components/FlexibleForm/FlexibleFormRow.tsx create mode 100644 airflow/ui/src/components/FlexibleForm/index.tsx diff --git a/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldBool.tsx b/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldBool.tsx new file mode 100644 index 0000000000000..ec3be663a6295 --- /dev/null +++ b/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldBool.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 { Switch } from "../ui"; + +export const isFieldBool = (fieldType: string) => fieldType === "boolean"; + +export const FlexibleFormFieldBool = ({ + key, + param, +}: FlexibleFormElementProps) => ( + +); diff --git a/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldDate.tsx b/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldDate.tsx new file mode 100644 index 0000000000000..6b9d22b27a814 --- /dev/null +++ b/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldDate.tsx @@ -0,0 +1,38 @@ +/*! + * 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 isFieldDate = (fieldType: string, fieldFormat: string | null) => + fieldType === "string" && fieldFormat === "date"; + +export const FlexibleFormFieldDate = ({ + key, + param, +}: FlexibleFormElementProps) => ( + +); diff --git a/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldDateTime.tsx b/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldDateTime.tsx new file mode 100644 index 0000000000000..0c6d0f1ebb9ec --- /dev/null +++ b/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldDateTime.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 { Input } from "@chakra-ui/react"; + +import type { FlexibleFormElementProps } from "."; + +export const isFieldDateTime = ( + fieldType: string, + fieldFormat: string | null, +) => fieldType === "string" && fieldFormat === "date-time"; + +export const FlexibleFormFieldDateTime = ({ + key, + param, +}: FlexibleFormElementProps) => ( + +); diff --git a/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldDropdown.tsx b/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldDropdown.tsx new file mode 100644 index 0000000000000..d097132a8769f --- /dev/null +++ b/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldDropdown.tsx @@ -0,0 +1,78 @@ +/*! + * 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 { Select } from "src/components/ui"; + +import type { FlexibleFormElementProps } from "."; + +export const isFieldDropdown = ( + fieldType: string, + fieldEnum: Array | null, +) => { + const enumTypes = ["string", "number", "integer"]; + + return enumTypes.includes(fieldType) && Array.isArray(fieldEnum); +}; + +const labelLookup = ( + key: string, + valuesDisplay: Record | null, +): string => { + if (valuesDisplay && typeof valuesDisplay === "object") { + return valuesDisplay[key] ?? key; + } + + return key; +}; + +export const FlexibleFormFieldDropdown = ({ + key, + param, +}: FlexibleFormElementProps) => { + const selectOptions = createListCollection({ + items: + param.schema.enum?.map((value) => ({ + label: labelLookup(value, param.schema.values_display), + value, + })) ?? [], + }); + + // TODO - somehow the dropdown is not working, does not give options :-( + return ( + + + + + + {selectOptions.items.map((option) => ( + + {option.label} + + ))} + + + ); +}; diff --git a/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldString.tsx b/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldString.tsx new file mode 100644 index 0000000000000..84c116b91a627 --- /dev/null +++ b/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldString.tsx @@ -0,0 +1,37 @@ +/*! + * 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 FlexibleFormFieldString = ({ + key, + param, +}: FlexibleFormElementProps) => ( + + // TODO: Add features missing from the current implementation + // - Maximum Length / Minimum Length + // - Examples as proposal +); diff --git a/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldTime.tsx b/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldTime.tsx new file mode 100644 index 0000000000000..f32c3bcd77e2b --- /dev/null +++ b/airflow/ui/src/components/FlexibleForm/FlexibleFormFieldTime.tsx @@ -0,0 +1,38 @@ +/*! + * 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 isFieldTime = (fieldType: string, fieldFormat: string | null) => + fieldType === "string" && fieldFormat === "date"; + +export const FlexibleFormFieldTime = ({ + key, + param, +}: FlexibleFormElementProps) => ( + +); diff --git a/airflow/ui/src/components/FlexibleForm/FlexibleFormHidden.tsx b/airflow/ui/src/components/FlexibleForm/FlexibleFormHidden.tsx new file mode 100644 index 0000000000000..19f6276f8a9a5 --- /dev/null +++ b/airflow/ui/src/components/FlexibleForm/FlexibleFormHidden.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 { VisuallyHidden } from "@chakra-ui/react"; + +import type { ParamSpec } from "src/queries/useDagParams"; + +import type { FlexibleFormElementProps } from "."; + +export const isHidden = (param: ParamSpec) => Boolean(param.schema.const); + +/** Render a "const" field where user can not change data as hidden */ +export const FlexibleFormHidden = ({ + key, + param, +}: FlexibleFormElementProps) => ( + + + +); diff --git a/airflow/ui/src/components/FlexibleForm/FlexibleFormNormalRow.tsx b/airflow/ui/src/components/FlexibleForm/FlexibleFormNormalRow.tsx new file mode 100644 index 0000000000000..ce315f74cb1a2 --- /dev/null +++ b/airflow/ui/src/components/FlexibleForm/FlexibleFormNormalRow.tsx @@ -0,0 +1,115 @@ +/*! + * 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 type { ParamSpec } from "src/queries/useDagParams"; + +import type { FlexibleFormElementProps } from "."; +import { FlexibleFormFieldBool, isFieldBool } from "./FlexibleFormFieldBool"; +import { FlexibleFormFieldDate, isFieldDate } from "./FlexibleFormFieldDate"; +import { + FlexibleFormFieldDateTime, + isFieldDateTime, +} from "./FlexibleFormFieldDateTime"; +import { + FlexibleFormFieldDropdown, + isFieldDropdown, +} from "./FlexibleFormFieldDropdown"; +import { FlexibleFormFieldString } from "./FlexibleFormFieldString"; +import { FlexibleFormFieldTime, isFieldTime } from "./FlexibleFormFieldTime"; + +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")); + +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; +}; + +export const FlexibleFormSelectElement = ({ + key, + param, +}: FlexibleFormElementProps) => { + // FUTURE: Add support for other types as described in AIP-68 via Plugins + const fieldType = inferType(param); + + if (isFieldBool(fieldType)) { + return ; + } else if (isFieldDateTime(fieldType, param.schema.format)) { + return ; + } else if (isFieldDate(fieldType, param.schema.format)) { + return ; + } else if (isFieldTime(fieldType, param.schema.format)) { + return ; + } else if (isFieldDropdown(fieldType, param.schema.enum)) { + return ; + } else { + // TODO other elements like number, integer, select etc. + // Missing: + // - (Simple) Array as Textarea + // - Array (as JSON via CodeMirror) + // - Multiple Select + // - Object (as JSON via CodeMirror) + // - Number (Into or generic number input) + // - Multiline Text + // see airflow/www/templates/airflow/trigger.html for logic in Airflow 2 + return ; + } +}; + +/** Render a normal form row with a field that is auto-selected */ +export const FlexibleFormNormalRow = ({ + key, + param, +}: FlexibleFormElementProps) => ( + + + + {param.schema.title ?? key} + + + + + + {param.description ?? ( + {param.schema.description_md} + )} + + + +); diff --git a/airflow/ui/src/components/FlexibleForm/FlexibleFormRow.tsx b/airflow/ui/src/components/FlexibleForm/FlexibleFormRow.tsx new file mode 100644 index 0000000000000..aae302dfc58e3 --- /dev/null +++ b/airflow/ui/src/components/FlexibleForm/FlexibleFormRow.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 { FlexibleFormHidden, isHidden } from "./FlexibleFormHidden"; +import { FlexibleFormNormalRow } from "./FlexibleFormNormalRow"; + +/** Generates a form row */ +export const FlexibleFormRow = ({ key, param }: FlexibleFormElementProps) => + isHidden(param) ? ( + + ) : ( + + ); diff --git a/airflow/ui/src/components/FlexibleForm/index.tsx b/airflow/ui/src/components/FlexibleForm/index.tsx new file mode 100644 index 0000000000000..aaa051f8c4b56 --- /dev/null +++ b/airflow/ui/src/components/FlexibleForm/index.tsx @@ -0,0 +1,43 @@ +/*! + * 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 { FlexibleFormRow } from "./FlexibleFormRow"; + +type FlexibleFormProps = { + readonly params: DagParamsSpec; +}; + +export type FlexibleFormElementProps = { + readonly key: string; + readonly param: ParamSpec; +}; + +const FlexibleForm = ({ params }: FlexibleFormProps) => ( + // TODO: Support multiple sections - at the moment all is rendered flat + }> + {Object.entries(params).map(([name, param]) => ( + + ))} + +); + +export default FlexibleForm; diff --git a/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx b/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx index 19f1aedbd0125..27683893393e5 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 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, @@ -130,6 +132,7 @@ const TriggerDAGForm = ({ dagId, onClose, open }: TriggerDAGFormProps) => { return ( <> + diff --git a/airflow/ui/src/queries/useDagParams.ts b/airflow/ui/src/queries/useDagParams.ts index bbb1cc282b85c..2cefdfa6d264a 100644 --- a/airflow/ui/src/queries/useDagParams.ts +++ b/airflow/ui/src/queries/useDagParams.ts @@ -19,10 +19,29 @@ import { useDagServiceGetDagDetails } from "openapi/queries"; import { toaster } from "src/components/ui"; +export type DagParamsSpec = Record; + +export type ParamSpec = { + description: string | null; + schema: { + const: string | null; + description_md: string | null; + enum: Array | null; + format: string | null; + title: string | null; + type: Array | string | null; + values_display: Record | null; + }; + value: unknown; +}; export const useDagParams = (dagId: string, open: boolean) => { - const { data, error } = useDagServiceGetDagDetails({ dagId }, undefined, { - enabled: open, - }); + const { + data, + error, + }: { data?: Record; error?: unknown } = + useDagServiceGetDagDetails({ dagId }, undefined, { + enabled: open, + }); if (Boolean(error)) { const errorDescription = @@ -39,14 +58,13 @@ 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.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 }; };