Skip to content

Commit

Permalink
Add Trigger DAG UI with advance options (#43367)
Browse files Browse the repository at this point in the history
* Initial draft

* some modifications

* rectifications

* initial react hook

* reset only on form change

* moving to diff componnent

* upgrading chakra ui 3.0 for TriggerDagForm.tsx

* upgrading chakra ui 3.0 for TriggerDagModal.tsx

* fix lint

* fix css

* add close

* dynamic theme for json

* lint fix

* simplify reset

* reviews

* json validation

* remove color schema

* use accordion

* Add Accordion component

* Use accordion component in triggeer form

* remove dag id from title

* fix response tracking

* add blue button

* making diff components for diff buttons

* lints

* lints fix

* variant add

* reset the form on close

* change type to string

* optimise

* post input validation and pretiffy for json

* modal refactor

* reviews

* add fiplay button

* replace logical with data interval

* add dag pause-upause toggle

* enable trigger for pause dag

* refactor

* DAG

---------

Co-authored-by: Brent Bovenzi <brent.bovenzi@gmail.com>
  • Loading branch information
shubhamraj-git and bbovenzi authored Nov 13, 2024
1 parent 8d1abf7 commit 52b12ad
Show file tree
Hide file tree
Showing 14 changed files with 1,311 additions and 25 deletions.
4 changes: 4 additions & 0 deletions airflow/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@
"dependencies": {
"@chakra-ui/anatomy": "^2.2.2",
"@chakra-ui/react": "^3.0.2",
"@codemirror/lang-json": "^6.0.1",
"@emotion/react": "^11.13.3",
"@tanstack/react-query": "^5.52.1",
"@tanstack/react-table": "^8.20.1",
"@uiw/react-codemirror": "^4.23.5",
"@uiw/codemirror-themes-all": "^4.23.5",
"axios": "^1.7.7",
"chakra-react-select": "6.0.0-next.2",
"dayjs": "^1.11.13",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.20.0",
"react-icons": "^5.3.0",
"react-router-dom": "^6.26.2",
"react-syntax-highlighter": "^15.5.6",
Expand Down
718 changes: 701 additions & 17 deletions airflow/ui/pnpm-lock.yaml

Large diffs are not rendered by default.

233 changes: 233 additions & 0 deletions airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/*!
* 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, Button, Box, Text, Spacer, HStack } 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 { useEffect, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { FiPlay } from "react-icons/fi";

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

import { Accordion } from "../ui";
import type { DagParams } from "./TriggerDag";

type TriggerDAGFormProps = {
dagParams: DagParams;
onClose: () => void;
onTrigger: (updatedDagParams: DagParams) => void;
setDagParams: React.Dispatch<React.SetStateAction<DagParams>>;
};

const TriggerDAGForm: React.FC<TriggerDAGFormProps> = ({
dagParams,
onTrigger,
setDagParams,
}) => {
const [jsonError, setJsonError] = useState<string | undefined>();

const {
control,
formState: { isDirty },
handleSubmit,
reset,
setValue,
watch,
} = useForm<DagParams>({
defaultValues: dagParams,
});

const dataIntervalStart = watch("dataIntervalStart");
const dataIntervalEnd = watch("dataIntervalEnd");

useEffect(() => {
reset(dagParams);
}, [dagParams, reset]);

const onSubmit = (data: DagParams) => {
onTrigger(data);
setDagParams(data);
setJsonError(undefined);
};

const validateAndPrettifyJson = (value: string) => {
try {
const parsedJson = JSON.parse(value) as JSON;

setJsonError(undefined);

return JSON.stringify(parsedJson, undefined, 2);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error occurred.";

setJsonError(`Invalid JSON format: ${errorMessage}`);

return value;
}
};

const validateDates = (
fieldName: "dataIntervalEnd" | "dataIntervalStart",
) => {
const startDate = dataIntervalStart
? new Date(dataIntervalStart)
: undefined;
const endDate = dataIntervalEnd ? new Date(dataIntervalEnd) : undefined;

if (startDate && endDate) {
if (fieldName === "dataIntervalStart" && startDate > endDate) {
setValue("dataIntervalStart", dataIntervalEnd);
} else if (fieldName === "dataIntervalEnd" && endDate < startDate) {
setValue("dataIntervalEnd", dataIntervalStart);
}
}
};

const { colorMode } = useColorMode();

return (
<>
<Accordion.Root collapsible size="lg" variant="enclosed">
<Accordion.Item key="advancedOptions" value="advancedOptions">
<Accordion.ItemTrigger cursor="button">
Advanced Options
</Accordion.ItemTrigger>
<Accordion.ItemContent>
<Box p={5}>
<Text fontSize="md" mb={2}>
Data Interval Start Date
</Text>
<Controller
control={control}
name="dataIntervalStart"
render={({ field }) => (
<Input
{...field}
max={dataIntervalEnd || undefined}
onBlur={() => validateDates("dataIntervalStart")}
placeholder="yyyy-mm-ddThh:mm"
size="sm"
type="datetime-local"
/>
)}
/>

<Text fontSize="md" mb={2} mt={6}>
Data Interval End Date
</Text>
<Controller
control={control}
name="dataIntervalEnd"
render={({ field }) => (
<Input
{...field}
min={dataIntervalStart || undefined}
onBlur={() => validateDates("dataIntervalEnd")}
placeholder="yyyy-mm-ddThh:mm"
size="sm"
type="datetime-local"
/>
)}
/>

<Text fontSize="md" mb={2} mt={6}>
Run ID
</Text>
<Controller
control={control}
name="runId"
render={({ field }) => (
<Input
{...field}
placeholder="Run id, optional - will be generated if not provided"
size="sm"
/>
)}
/>

<Text fontSize="md" mb={2} mt={6}>
Configuration JSON
</Text>
<Controller
control={control}
name="configJson"
render={({ field }) => (
<Box mb={4}>
<CodeMirror
{...field}
basicSetup={{
autocompletion: true,
bracketMatching: true,
foldGutter: true,
lineNumbers: true,
}}
extensions={[json()]}
height="200px"
onBlur={() => {
const prettifiedJson = validateAndPrettifyJson(
field.value,
);

field.onChange(prettifiedJson);
}}
style={{
border: "1px solid #CBD5E0",
borderRadius: "8px",
outline: "none",
padding: "2px",
}}
theme={colorMode === "dark" ? githubDark : githubLight}
/>
{Boolean(jsonError) ? (
<Text color="red.500" fontSize="sm" mt={2}>
{jsonError}
</Text>
) : undefined}
</Box>
)}
/>
</Box>
</Accordion.ItemContent>
</Accordion.Item>
</Accordion.Root>

<Box as="footer" display="flex" justifyContent="flex-end" mt={4}>
<HStack w="full">
{isDirty ? (
<Button onClick={() => reset()} variant="outline">
Reset
</Button>
) : undefined}
<Spacer />
<Button
colorPalette="blue"
disabled={Boolean(jsonError)}
onClick={() => void handleSubmit(onSubmit)()}
>
<FiPlay /> Trigger
</Button>
</HStack>
</Box>
</>
);
};

export default TriggerDAGForm;
51 changes: 51 additions & 0 deletions airflow/ui/src/components/TriggerDag/TriggerDAGIconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*!
* 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 { Box, Button } from "@chakra-ui/react";
import { useDisclosure } from "@chakra-ui/react";
import { FiPlay } from "react-icons/fi";

import type { DAGWithLatestDagRunsResponse } from "openapi/requests/types.gen";

import TriggerDAGModal from "./TriggerDAGModal";

type Props = {
readonly dag: DAGWithLatestDagRunsResponse;
};

const TriggerDAGIconButton: React.FC<Props> = ({ dag }) => {
const { onClose, onOpen, open } = useDisclosure();

return (
<Box>
<Button onClick={onOpen} variant="ghost">
<FiPlay />
</Button>

<TriggerDAGModal
dagDisplayName={dag.dag_display_name}
dagId={dag.dag_id}
isPaused={dag.is_paused}
onClose={onClose}
open={open}
/>
</Box>
);
};

export default TriggerDAGIconButton;
Loading

0 comments on commit 52b12ad

Please sign in to comment.