From 282063b33aabe18014ce0b1685ef282a186dfdb9 Mon Sep 17 00:00:00 2001 From: Pierre Jeambrun Date: Sun, 12 Jan 2025 07:56:17 +0800 Subject: [PATCH] AIP-38 Clear Tasks (#45559) * POC working * Add clear task instance action button * Harmonize actions columns in table * Simplify state management by using useEffect * Rename ClearRun folder to Clear * Factorize columns * Better file structure * Refactor SegmentedControl Component to handle multiple values --- .../ClearAccordion.tsx} | 54 +------ .../Run}/ClearRunButton.tsx | 15 +- .../Run}/ClearRunDialog.tsx | 68 +++----- .../{ClearRun => Clear/Run}/index.tsx | 0 .../TaskInstance/ClearTaskInstanceButton.tsx | 74 +++++++++ .../TaskInstance/ClearTaskInstanceDialog.tsx | 153 ++++++++++++++++++ .../components/Clear/TaskInstance/index.tsx | 20 +++ airflow/ui/src/components/Clear/columns.tsx | 64 ++++++++ airflow/ui/src/components/Clear/index.tsx | 21 +++ .../ui/src/components/ui/SegmentedControl.tsx | 71 +++++--- airflow/ui/src/pages/Dag/Runs/Runs.tsx | 7 +- airflow/ui/src/pages/Run/Header.tsx | 6 +- airflow/ui/src/pages/Run/TaskInstances.tsx | 16 +- airflow/ui/src/pages/TaskInstance/Header.tsx | 2 + .../ui/src/queries/useClearTaskInstances.ts | 92 +++++++++++ .../ui/src/queries/usePatchTaskInstance.ts | 64 ++++++++ 16 files changed, 587 insertions(+), 140 deletions(-) rename airflow/ui/src/components/{ClearRun/ClearRunTaskAccordion.tsx => Clear/ClearAccordion.tsx} (69%) rename airflow/ui/src/components/{ClearRun => Clear/Run}/ClearRunButton.tsx (85%) rename airflow/ui/src/components/{ClearRun => Clear/Run}/ClearRunDialog.tsx (72%) rename airflow/ui/src/components/{ClearRun => Clear/Run}/index.tsx (100%) create mode 100644 airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx create mode 100644 airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceDialog.tsx create mode 100644 airflow/ui/src/components/Clear/TaskInstance/index.tsx create mode 100644 airflow/ui/src/components/Clear/columns.tsx create mode 100644 airflow/ui/src/components/Clear/index.tsx create mode 100644 airflow/ui/src/queries/useClearTaskInstances.ts create mode 100644 airflow/ui/src/queries/usePatchTaskInstance.ts diff --git a/airflow/ui/src/components/ClearRun/ClearRunTaskAccordion.tsx b/airflow/ui/src/components/Clear/ClearAccordion.tsx similarity index 69% rename from airflow/ui/src/components/ClearRun/ClearRunTaskAccordion.tsx rename to airflow/ui/src/components/Clear/ClearAccordion.tsx index b8dadb71c0ac3..74be262da7561 100644 --- a/airflow/ui/src/components/ClearRun/ClearRunTaskAccordion.tsx +++ b/airflow/ui/src/components/Clear/ClearAccordion.tsx @@ -17,60 +17,14 @@ * under the License. */ import { Box, Editable, Text, VStack } from "@chakra-ui/react"; -import { Link } from "@chakra-ui/react"; -import type { ColumnDef } from "@tanstack/react-table"; import type { ChangeEvent } from "react"; -import { Link as RouterLink } from "react-router-dom"; -import type { - DAGRunResponse, - TaskInstanceCollectionResponse, - TaskInstanceResponse, -} from "openapi/requests/types.gen"; +import type { DAGRunResponse, TaskInstanceCollectionResponse } from "openapi/requests/types.gen"; import { DataTable } from "src/components/DataTable"; import ReactMarkdown from "src/components/ReactMarkdown"; -import { Status, Tooltip } from "src/components/ui"; import { Accordion } from "src/components/ui"; -import { getTaskInstanceLink } from "src/utils/links"; -import { trimText } from "src/utils/trimTextFn"; -const columns: Array> = [ - { - accessorKey: "task_display_name", - cell: ({ row: { original } }) => ( - - - - {trimText(original.task_display_name, 25).trimmedText} - - - - ), - enableSorting: false, - header: "Task ID", - }, - { - accessorKey: "state", - cell: ({ - row: { - original: { state }, - }, - }) => {state}, - enableSorting: false, - header: () => "State", - }, - { - accessorFn: (row: TaskInstanceResponse) => row.rendered_map_index ?? row.map_index, - enableSorting: false, - header: "Map Index", - }, - - { - accessorKey: "dag_run_id", - enableSorting: false, - header: "Run Id", - }, -]; +import { columns } from "./columns"; type Props = { readonly affectedTasks?: TaskInstanceCollectionResponse; @@ -80,7 +34,7 @@ type Props = { // Table is in memory, pagination and sorting are disabled. // TODO: Make a front-end only unconnected table component with client side ordering and pagination -const ClearRunTasksAccordion = ({ affectedTasks, note, setNote }: Props) => ( +const ClearAccordion = ({ affectedTasks, note, setNote }: Props) => ( @@ -144,4 +98,4 @@ const ClearRunTasksAccordion = ({ affectedTasks, note, setNote }: Props) => ( ); -export default ClearRunTasksAccordion; +export default ClearAccordion; diff --git a/airflow/ui/src/components/ClearRun/ClearRunButton.tsx b/airflow/ui/src/components/Clear/Run/ClearRunButton.tsx similarity index 85% rename from airflow/ui/src/components/ClearRun/ClearRunButton.tsx rename to airflow/ui/src/components/Clear/Run/ClearRunButton.tsx index c07cccebd26c0..151e2f3bd9e92 100644 --- a/airflow/ui/src/components/ClearRun/ClearRunButton.tsx +++ b/airflow/ui/src/components/Clear/Run/ClearRunButton.tsx @@ -21,9 +21,9 @@ import { useState } from "react"; import { FiRefreshCw } from "react-icons/fi"; import type { DAGRunResponse, TaskInstanceCollectionResponse } from "openapi/requests/types.gen"; +import ActionButton from "src/components/ui/ActionButton"; import { useClearDagRun } from "src/queries/useClearRun"; -import ActionButton from "../ui/ActionButton"; import ClearRunDialog from "./ClearRunDialog"; type Props = { @@ -34,8 +34,6 @@ type Props = { const ClearRunButton = ({ dagRun, withText = true }: Props) => { const { onClose, onOpen, open } = useDisclosure(); - const [onlyFailed, setOnlyFailed] = useState(false); - const [affectedTasks, setAffectedTasks] = useState({ task_instances: [], total_entries: 0, @@ -56,14 +54,7 @@ const ClearRunButton = ({ dagRun, withText = true }: Props) => { } - onClick={() => { - onOpen(); - mutate({ - dagId, - dagRunId, - requestBody: { dry_run: true, only_failed: onlyFailed }, - }); - }} + onClick={onOpen} text="Clear Run" withText={withText} /> @@ -74,9 +65,7 @@ const ClearRunButton = ({ dagRun, withText = true }: Props) => { isPending={isPending} mutate={mutate} onClose={onClose} - onlyFailed={onlyFailed} open={open} - setOnlyFailed={setOnlyFailed} /> ); diff --git a/airflow/ui/src/components/ClearRun/ClearRunDialog.tsx b/airflow/ui/src/components/Clear/Run/ClearRunDialog.tsx similarity index 72% rename from airflow/ui/src/components/ClearRun/ClearRunDialog.tsx rename to airflow/ui/src/components/Clear/Run/ClearRunDialog.tsx index c77b0873c598b..4cb48669b5537 100644 --- a/airflow/ui/src/components/ClearRun/ClearRunDialog.tsx +++ b/airflow/ui/src/components/Clear/Run/ClearRunDialog.tsx @@ -17,7 +17,7 @@ * under the License. */ import { Flex, Heading, VStack } from "@chakra-ui/react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { FiRefreshCw } from "react-icons/fi"; import type { @@ -26,10 +26,10 @@ import type { TaskInstanceCollectionResponse, } from "openapi/requests/types.gen"; import { Button, Dialog } from "src/components/ui"; +import SegmentedControl from "src/components/ui/SegmentedControl"; import { usePatchDagRun } from "src/queries/usePatchDagRun"; -import SegmentedControl from "../ui/SegmentedControl"; -import ClearRunTasksAccordion from "./ClearRunTaskAccordion"; +import ClearAccordion from "../ClearAccordion"; type Props = { readonly affectedTasks: TaskInstanceCollectionResponse; @@ -45,57 +45,36 @@ type Props = { requestBody: DAGRunClearBody; }) => void; readonly onClose: () => void; - readonly onlyFailed: boolean; readonly open: boolean; - readonly setOnlyFailed: (value: boolean) => void; }; -const ClearRunDialog = ({ - affectedTasks, - dagRun, - isPending, - mutate, - onClose, - onlyFailed, - open, - setOnlyFailed, -}: Props) => { +const ClearRunDialog = ({ affectedTasks, dagRun, isPending, mutate, onClose, open }: Props) => { + const [selectedOptions, setSelectedOptions] = useState>([]); + + const onlyFailed = selectedOptions.includes("onlyFailed"); + const dagId = dagRun.dag_id; const dagRunId = dagRun.dag_run_id; const [note, setNote] = useState(dagRun.note); const { isPending: isPendingPatchDagRun, mutate: mutatePatchDagRun } = usePatchDagRun({ dagId, dagRunId }); - const onChange = (value: string) => { - switch (value) { - case "existing_tasks": - setOnlyFailed(false); - mutate({ - dagId, - dagRunId, - requestBody: { dry_run: true, only_failed: false }, - }); - break; - case "only_failed": - setOnlyFailed(true); - mutate({ - dagId, - dagRunId, - requestBody: { dry_run: true, only_failed: true }, - }); - break; - default: - // TODO: Handle this `new_tasks` case - break; - } - }; + useEffect(() => { + mutate({ + dagId, + dagRunId, + requestBody: { dry_run: true, only_failed: onlyFailed }, + }); + }, [dagId, dagRunId, mutate, onlyFailed]); return ( - Clear DagRun - {dagRunId} + + Clear DagRun: {dagRunId} + @@ -104,21 +83,20 @@ const ClearRunDialog = ({ - + + + + + + ); +}; + +export default ClearTaskInstanceDialog; diff --git a/airflow/ui/src/components/Clear/TaskInstance/index.tsx b/airflow/ui/src/components/Clear/TaskInstance/index.tsx new file mode 100644 index 0000000000000..0887cd5dcda58 --- /dev/null +++ b/airflow/ui/src/components/Clear/TaskInstance/index.tsx @@ -0,0 +1,20 @@ +/*! + * 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. + */ + +export { default } from "./ClearTaskInstanceButton"; diff --git a/airflow/ui/src/components/Clear/columns.tsx b/airflow/ui/src/components/Clear/columns.tsx new file mode 100644 index 0000000000000..e2fa272f4339b --- /dev/null +++ b/airflow/ui/src/components/Clear/columns.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 { Link } from "@chakra-ui/react"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Link as RouterLink } from "react-router-dom"; + +import type { TaskInstanceResponse } from "openapi/requests/types.gen"; +import { Status, Tooltip } from "src/components/ui"; +import { getTaskInstanceLink } from "src/utils/links"; +import { trimText } from "src/utils/trimTextFn"; + +export const columns: Array> = [ + { + accessorKey: "task_display_name", + cell: ({ row: { original } }) => ( + + + + {trimText(original.task_display_name, 25).trimmedText} + + + + ), + enableSorting: false, + header: "Task ID", + }, + { + accessorKey: "state", + cell: ({ + row: { + original: { state }, + }, + }) => {state}, + enableSorting: false, + header: () => "State", + }, + { + accessorFn: (row) => row.rendered_map_index ?? row.map_index, + enableSorting: false, + header: "Map Index", + }, + + { + accessorKey: "dag_run_id", + enableSorting: false, + header: "Run Id", + }, +]; diff --git a/airflow/ui/src/components/Clear/index.tsx b/airflow/ui/src/components/Clear/index.tsx new file mode 100644 index 0000000000000..3183e196c5199 --- /dev/null +++ b/airflow/ui/src/components/Clear/index.tsx @@ -0,0 +1,21 @@ +/*! + * 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. + */ + +export { default as ClearRunButton } from "./Run"; +export { default as ClearTaskInstanceButton } from "./TaskInstance"; diff --git a/airflow/ui/src/components/ui/SegmentedControl.tsx b/airflow/ui/src/components/ui/SegmentedControl.tsx index 4d2bb2d074ef3..ac19115779826 100644 --- a/airflow/ui/src/components/ui/SegmentedControl.tsx +++ b/airflow/ui/src/components/ui/SegmentedControl.tsx @@ -16,37 +16,58 @@ * specific language governing permissions and limitations * under the License. */ -import { Tabs, For, type TabsRootProps } from "@chakra-ui/react"; +import { Button, Group } from "@chakra-ui/react"; +import { useEffect, useState } from "react"; type Option = { - disabled?: boolean; - label: string; - value: string; + readonly disabled?: boolean; + readonly label: string; + readonly value: string; }; type SegmentedControlProps = { - readonly onValueChange: (value: string) => void; + readonly defaultValues?: Array; + readonly multiple?: boolean; + readonly onChange?: (options: Array) => void; readonly options: Array