Skip to content

Commit

Permalink
Connect Trigger UI Conf with the flexible form (#45790)
Browse files Browse the repository at this point in the history
* centralised setup

* refactor

* remove param pass

* remove reset

* date time fix

* refactor

* accordion css

* remove console error

* copy params

* reviews

* css alert

* fix

* reviews regarding setting null

* refactor

* Empty Objects sets to Null

Co-authored-by: Jens Scheffler <[email protected]>

* Fix Date and time

Co-authored-by: Jens Scheffler <[email protected]>

---------

Co-authored-by: Jens Scheffler <[email protected]>
  • Loading branch information
shubhamraj-git and jscheffl authored Jan 25, 2025
1 parent 961ee6e commit ba7135c
Show file tree
Hide file tree
Showing 22 changed files with 623 additions and 219 deletions.
3 changes: 2 additions & 1 deletion airflow/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"react-syntax-highlighter": "^15.5.6",
"remark-gfm": "^4.0.0",
"use-debounce": "^10.0.3",
"usehooks-ts": "^3.1.0"
"usehooks-ts": "^3.1.0",
"zustand": "^5.0.3"
},
"devDependencies": {
"@7nohe/openapi-react-query-codegen": "^1.6.0",
Expand Down
27 changes: 27 additions & 0 deletions airflow/ui/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

97 changes: 76 additions & 21 deletions airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,92 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Text } from "@chakra-ui/react";
import { json } from "@codemirror/lang-json";
import { githubLight, githubDark } from "@uiw/codemirror-themes-all";
import CodeMirror from "@uiw/react-codemirror";
import { useState } from "react";

import { useColorMode } from "src/context/colorMode";

import type { FlexibleFormElementProps } from ".";
import { paramPlaceholder, useParamStore } from "../TriggerDag/useParamStore";

export const FieldAdvancedArray = ({ name, param }: FlexibleFormElementProps) => {
export const FieldAdvancedArray = ({ name }: FlexibleFormElementProps) => {
const { colorMode } = useColorMode();
const { paramsDict, setParamsDict } = useParamStore();
const param = paramsDict[name] ?? paramPlaceholder;
const [error, setError] = useState<unknown>(undefined);
// Determine the expected type based on schema
const expectedType = param.schema.items?.type ?? "object";

const handleChange = (value: string) => {
setError(undefined);
if (value === "") {
if (paramsDict[name]) {
// "undefined" values are removed from params, so we set it to null to avoid falling back to DAG defaults.
// eslint-disable-next-line unicorn/no-null
paramsDict[name].value = null;
}
setParamsDict(paramsDict);
} else {
try {
const parsedValue = JSON.parse(value) as unknown;

if (!Array.isArray(parsedValue)) {
throw new TypeError("Value must be an array.");
}

if (expectedType === "number" && !parsedValue.every((item) => typeof item === "number")) {
// Ensure all elements in the array are numbers
throw new TypeError("All elements in the array must be numbers.");
} else if (
expectedType === "object" &&
!parsedValue.every((item) => typeof item === "object" && item !== null)
) {
// Ensure all elements in the array are objects
throw new TypeError("All elements in the array must be objects.");
}

if (paramsDict[name]) {
paramsDict[name].value = parsedValue;
}

setParamsDict(paramsDict);
} catch (_error) {
setError(expectedType === "number" ? String(_error).replace("JSON", "Array") : _error);
}
}
};

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)}
/>
<>
<CodeMirror
basicSetup={{
autocompletion: true,
bracketMatching: true,
foldGutter: true,
lineNumbers: true,
}}
extensions={[json()]}
height="200px"
id={`element_${name}`}
onChange={handleChange}
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)}
/>
{Boolean(error) ? (
<Text color="red.solid" fontSize="xs">
{String(error)}
</Text>
) : undefined}
</>
);
};
30 changes: 22 additions & 8 deletions airflow/ui/src/components/FlexibleForm/FieldBool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,27 @@
* under the License.
*/
import type { FlexibleFormElementProps } from ".";
import { paramPlaceholder, useParamStore } from "../TriggerDag/useParamStore";
import { Switch } from "../ui";

export const FieldBool = ({ name, param }: FlexibleFormElementProps) => (
<Switch
colorPalette="blue"
defaultChecked={Boolean(param.value)}
id={`element_${name}`}
name={`element_${name}`}
/>
);
export const FieldBool = ({ name }: FlexibleFormElementProps) => {
const { paramsDict, setParamsDict } = useParamStore();
const param = paramsDict[name] ?? paramPlaceholder;
const onCheck = (value: boolean) => {
if (paramsDict[name]) {
paramsDict[name].value = value;
}

setParamsDict(paramsDict);
};

return (
<Switch
checked={Boolean(param.value)}
colorPalette="blue"
id={`element_${name}`}
name={`element_${name}`}
onCheckedChange={(event) => onCheck(event.checked)}
/>
);
};
40 changes: 31 additions & 9 deletions airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,35 @@
import { Input, type InputProps } from "@chakra-ui/react";

import type { FlexibleFormElementProps } from ".";
import { paramPlaceholder, useParamStore } from "../TriggerDag/useParamStore";

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}
/>
);
export const FieldDateTime = ({ name, ...rest }: FlexibleFormElementProps & InputProps) => {
const { paramsDict, setParamsDict } = useParamStore();
const param = paramsDict[name] ?? paramPlaceholder;
const handleChange = (value: string) => {
if (paramsDict[name]) {
if (rest.type === "datetime-local") {
// "undefined" values are removed from params, so we set it to null to avoid falling back to DAG defaults.
// eslint-disable-next-line unicorn/no-null
paramsDict[name].value = value === "" ? null : `${value}:00+00:00`; // Need to suffix to make it UTC like
} else {
// "undefined" values are removed from params, so we set it to null to avoid falling back to DAG defaults.
// eslint-disable-next-line unicorn/no-null
paramsDict[name].value = value === "" ? null : value;
}
}

setParamsDict(paramsDict);
};

return (
<Input
id={`element_${name}`}
name={`element_${name}`}
onChange={(event) => handleChange(event.target.value)}
size="sm"
type={rest.type}
value={param.value !== null && param.value !== undefined ? String(param.value).slice(0, 16) : ""}
/>
);
};
22 changes: 19 additions & 3 deletions airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useRef } from "react";
import { Select } from "src/components/ui";

import type { FlexibleFormElementProps } from ".";
import { paramPlaceholder, useParamStore } from "../TriggerDag/useParamStore";

const labelLookup = (key: string, valuesDisplay: Record<string, string> | undefined): string => {
if (valuesDisplay && typeof valuesDisplay === "object") {
Expand All @@ -32,26 +33,41 @@ const labelLookup = (key: string, valuesDisplay: Record<string, string> | undefi
};
const enumTypes = ["string", "number", "integer"];

export const FieldDropdown = ({ name, param }: FlexibleFormElementProps) => {
export const FieldDropdown = ({ name }: FlexibleFormElementProps) => {
const { paramsDict, setParamsDict } = useParamStore();
const param = paramsDict[name] ?? paramPlaceholder;

const selectOptions = createListCollection({
items:
param.schema.enum?.map((value) => ({
label: labelLookup(value, param.schema.values_display),
value,
})) ?? [],
});

const contentRef = useRef<HTMLDivElement>(null);

const handleChange = ([value]: Array<string>) => {
if (paramsDict[name]) {
// "undefined" values are removed from params, so we set it to null to avoid falling back to DAG defaults.
// eslint-disable-next-line unicorn/no-null
paramsDict[name].value = value ?? null;
}

setParamsDict(paramsDict);
};

return (
<Select.Root
collection={selectOptions}
defaultValue={enumTypes.includes(typeof param.value) ? [String(param.value)] : undefined}
id={`element_${name}`}
name={`element_${name}`}
onValueChange={(event) => handleChange(event.value)}
ref={contentRef}
size="sm"
value={enumTypes.includes(typeof param.value) ? [param.value as string] : undefined}
>
<Select.Trigger>
<Select.Trigger clearable>
<Select.ValueText placeholder="Select Value" />
</Select.Trigger>
<Select.Content portalRef={contentRef}>
Expand Down
32 changes: 29 additions & 3 deletions airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Select as ReactSelect } from "chakra-react-select";
import { type MultiValue, Select as ReactSelect } from "chakra-react-select";
import { useState } from "react";

import type { FlexibleFormElementProps } from ".";
import { paramPlaceholder, useParamStore } from "../TriggerDag/useParamStore";

const labelLookup = (key: string, valuesDisplay: Record<string, string> | undefined): string => {
if (valuesDisplay && typeof valuesDisplay === "object") {
Expand All @@ -29,7 +30,11 @@ const labelLookup = (key: string, valuesDisplay: Record<string, string> | undefi
return key;
};

export const FieldMultiSelect = ({ name, param }: FlexibleFormElementProps) => {
export const FieldMultiSelect = ({ name }: FlexibleFormElementProps) => {
const { paramsDict, setParamsDict } = useParamStore();
const param = paramsDict[name] ?? paramPlaceholder;

// Initialize `selectedOptions` directly from `paramsDict`
const [selectedOptions, setSelectedOptions] = useState(
Array.isArray(param.value)
? (param.value as Array<string>).map((value) => ({
Expand All @@ -39,14 +44,35 @@ export const FieldMultiSelect = ({ name, param }: FlexibleFormElementProps) => {
: [],
);

// Handle changes to the select field
const handleChange = (
newValue: MultiValue<{
label: string;
value: string;
}>,
) => {
const updatedOptions = [...newValue];

setSelectedOptions(updatedOptions);

// "undefined" values are removed from params, so we set it to null to avoid falling back to DAG defaults.
// eslint-disable-next-line unicorn/no-null
const newValueArray = updatedOptions.length ? updatedOptions.map((option) => option.value) : null;

if (paramsDict[name]) {
paramsDict[name].value = newValueArray;
}
setParamsDict(paramsDict);
};

return (
<ReactSelect
aria-label="Select one or multiple values"
id={`element_${name}`}
isClearable
isMulti
name={`element_${name}`}
onChange={(newValue) => setSelectedOptions([...newValue])}
onChange={handleChange}
options={
param.schema.examples?.map((value) => ({
label: labelLookup(value, param.schema.values_display),
Expand Down
Loading

0 comments on commit ba7135c

Please sign in to comment.