This is an automated email from the ASF dual-hosted git repository. bbovenzi pushed a commit to branch mapped-instance-actions in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 9158367d7cf9171832fd10eaa9b6fedd83aef2b8 Author: Brent Bovenzi <[email protected]> AuthorDate: Sat Apr 9 14:50:23 2022 -0400 Allow bulk mapped task actions --- airflow/www/static/js/tree/Table.jsx | 44 ++++++++++++++++++++-- .../content/taskInstance/MappedInstances.jsx | 3 +- .../js/tree/details/content/taskInstance/index.jsx | 25 +++++++++++- .../content/taskInstance/taskActions/Clear.jsx | 2 + .../taskInstance/taskActions/MarkFailed.jsx | 3 +- .../taskInstance/taskActions/MarkSuccess.jsx | 4 +- .../content/taskInstance/taskActions/Run.jsx | 22 ++++++++--- 7 files changed, 88 insertions(+), 15 deletions(-) diff --git a/airflow/www/static/js/tree/Table.jsx b/airflow/www/static/js/tree/Table.jsx index 06eb84cad4..32119cf50e 100644 --- a/airflow/www/static/js/tree/Table.jsx +++ b/airflow/www/static/js/tree/Table.jsx @@ -21,7 +21,7 @@ * Custom wrapper of react-table using Chakra UI components */ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef, forwardRef } from 'react'; import { Flex, Table as ChakraTable, @@ -33,9 +33,10 @@ import { IconButton, Text, useColorModeValue, + Checkbox, } from '@chakra-ui/react'; import { - useTable, useSortBy, usePagination, + useTable, useSortBy, usePagination, useRowSelect, } from 'react-table'; import { MdKeyboardArrowLeft, MdKeyboardArrowRight, @@ -44,8 +45,23 @@ import { TiArrowUnsorted, TiArrowSortedDown, TiArrowSortedUp, } from 'react-icons/ti'; +const IndeterminateCheckbox = forwardRef( + ({ indeterminate, ...rest }, ref) => { + const defaultRef = useRef(); + const resolvedRef = ref || defaultRef; + + useEffect(() => { + resolvedRef.current.indeterminate = indeterminate; + }, [resolvedRef, indeterminate]); + + return ( + <Checkbox ref={resolvedRef} {...rest} /> + ); + }, +); + const Table = ({ - data, columns, manualPagination, pageSize = 25, setSortBy, isLoading = false, + data, columns, manualPagination, pageSize = 25, setSortBy, isLoading = false, selectRows, }) => { const { totalEntries, offset, setOffset } = manualPagination || {}; const oddColor = useColorModeValue('gray.50', 'gray.900'); @@ -66,7 +82,8 @@ const Table = ({ canNextPage, nextPage, previousPage, - state: { pageIndex, sortBy }, + selectedFlatRows, + state: { pageIndex, sortBy, selectedRowIds }, } = useTable( { columns, @@ -81,6 +98,20 @@ const Table = ({ }, useSortBy, usePagination, + useRowSelect, + (hooks) => { + hooks.visibleColumns.push((cols) => [ + { + id: 'selection', + Cell: ({ row }) => ( + <div> + <IndeterminateCheckbox {...row.getToggleRowSelectedProps()} /> + </div> + ), + }, + ...cols, + ]); + }, ); const handleNext = () => { @@ -97,6 +128,11 @@ const Table = ({ if (setSortBy) setSortBy(sortBy); }, [sortBy, setSortBy]); + useEffect(() => { + if (selectRows) selectRows(selectedFlatRows.map((row) => row.original.mapIndex)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedRowIds, selectRows]); + return ( <> <ChakraTable {...getTableProps()}> diff --git a/airflow/www/static/js/tree/details/content/taskInstance/MappedInstances.jsx b/airflow/www/static/js/tree/details/content/taskInstance/MappedInstances.jsx index 42bbdca66f..77c0713ab3 100644 --- a/airflow/www/static/js/tree/details/content/taskInstance/MappedInstances.jsx +++ b/airflow/www/static/js/tree/details/content/taskInstance/MappedInstances.jsx @@ -46,7 +46,7 @@ const IconLink = (props) => ( ); const MappedInstances = ({ - dagId, runId, taskId, + dagId, runId, taskId, selectRows, }) => { const limit = 25; const [offset, setOffset] = useState(0); @@ -147,6 +147,7 @@ const MappedInstances = ({ pageSize={limit} setSortBy={setSortBy} isLoading={isLoading} + selectRows={selectRows} /> </Box> ); diff --git a/airflow/www/static/js/tree/details/content/taskInstance/index.jsx b/airflow/www/static/js/tree/details/content/taskInstance/index.jsx index d8b71cb128..62ffee156a 100644 --- a/airflow/www/static/js/tree/details/content/taskInstance/index.jsx +++ b/airflow/www/static/js/tree/details/content/taskInstance/index.jsx @@ -17,12 +17,14 @@ * under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { Box, VStack, Divider, StackDivider, + Text, + Flex, } from '@chakra-ui/react'; import RunAction from './taskActions/Run'; @@ -54,6 +56,7 @@ const getTask = ({ taskId, runId, task }) => { }; const TaskInstance = ({ taskId, runId }) => { + const [selectedRows, setSelectedRows] = useState([]); const { data: { groups = {}, dagRuns = [] } } = useTreeData(); const group = getTask({ taskId, runId, task: groups }); const run = dagRuns.find((r) => r.runId === runId); @@ -68,6 +71,11 @@ const TaskInstance = ({ taskId, runId }) => { const instance = group.instances.find((ti) => ti.runId === runId); + let taskActionsTitle = 'Task Actions'; + if (isMapped) { + taskActionsTitle += ` for ${selectedRows.length || 'all'} mapped task${selectedRows.length !== 1 ? 's' : ''}`; + } + return ( <Box py="4px"> {!isGroup && ( @@ -80,27 +88,40 @@ const TaskInstance = ({ taskId, runId }) => { )} {!isGroup && ( <Box my={3}> + <Text as="strong">{taskActionsTitle}</Text> + <Flex maxHeight="20px" minHeight="20px"> + {selectedRows.length ? ( + <Text color="red.500"> + Clear, Mark Failed, and Mark Success do not yet work with individual mapped tasks. + </Text> + ) : <Divider my={2} />} + </Flex> + {/* visibility={selectedRows.length ? 'visible' : 'hidden'} */} <VStack justifyContent="center" divider={<StackDivider my={3} />}> <RunAction runId={runId} taskId={taskId} dagId={dagId} + selectedRows={selectedRows} /> <ClearAction runId={runId} taskId={taskId} dagId={dagId} executionDate={executionDate} + selectedRows={selectedRows} /> <MarkFailedAction runId={runId} taskId={taskId} dagId={dagId} + selectedRows={selectedRows} /> <MarkSuccessAction runId={runId} taskId={taskId} dagId={dagId} + selectedRows={selectedRows} /> </VStack> <Divider my={2} /> @@ -122,7 +143,7 @@ const TaskInstance = ({ taskId, runId }) => { extraLinks={extraLinks} /> {isMapped && ( - <MappedInstances dagId={dagId} runId={runId} taskId={taskId} /> + <MappedInstances dagId={dagId} runId={runId} taskId={taskId} selectRows={setSelectedRows} /> )} </Box> ); diff --git a/airflow/www/static/js/tree/details/content/taskInstance/taskActions/Clear.jsx b/airflow/www/static/js/tree/details/content/taskInstance/taskActions/Clear.jsx index 4196edc6b9..cada7b59ed 100644 --- a/airflow/www/static/js/tree/details/content/taskInstance/taskActions/Clear.jsx +++ b/airflow/www/static/js/tree/details/content/taskInstance/taskActions/Clear.jsx @@ -34,6 +34,7 @@ const Run = ({ runId, taskId, executionDate, + selectedRows, }) => { const [affectedTasks, setAffectedTasks] = useState([]); @@ -113,6 +114,7 @@ const Run = ({ colorScheme="blue" onClick={onClick} isLoading={isLoading} + isDisabled={!!selectedRows.length} title="Clearing deletes the previous state of the task instance, allowing it to get re-triggered by the scheduler or a backfill command" > Clear diff --git a/airflow/www/static/js/tree/details/content/taskInstance/taskActions/MarkFailed.jsx b/airflow/www/static/js/tree/details/content/taskInstance/taskActions/MarkFailed.jsx index fe277c9eef..6bc10c066e 100644 --- a/airflow/www/static/js/tree/details/content/taskInstance/taskActions/MarkFailed.jsx +++ b/airflow/www/static/js/tree/details/content/taskInstance/taskActions/MarkFailed.jsx @@ -33,6 +33,7 @@ const MarkFailed = ({ dagId, runId, taskId, + selectedRows, }) => { const [affectedTasks, setAffectedTasks] = useState([]); @@ -99,7 +100,7 @@ const MarkFailed = ({ <ActionButton bg={upstream && 'gray.100'} onClick={onToggleUpstream} name="Upstream" /> <ActionButton bg={downstream && 'gray.100'} onClick={onToggleDownstream} name="Downstream" /> </ButtonGroup> - <Button colorScheme="red" onClick={onClick} isLoading={isMarkLoading || isConfirmLoading}> + <Button colorScheme="red" onClick={onClick} isLoading={isMarkLoading || isConfirmLoading} isDisabled={!!selectedRows.length}> Mark Failed </Button> <ConfirmDialog diff --git a/airflow/www/static/js/tree/details/content/taskInstance/taskActions/MarkSuccess.jsx b/airflow/www/static/js/tree/details/content/taskInstance/taskActions/MarkSuccess.jsx index 06bc80c756..b4d2b8c047 100644 --- a/airflow/www/static/js/tree/details/content/taskInstance/taskActions/MarkSuccess.jsx +++ b/airflow/www/static/js/tree/details/content/taskInstance/taskActions/MarkSuccess.jsx @@ -30,7 +30,7 @@ import ActionButton from './ActionButton'; import { useMarkSuccessTask, useConfirmMarkTask } from '../../../../api'; const Run = ({ - dagId, runId, taskId, + dagId, runId, taskId, selectedRows, }) => { const [affectedTasks, setAffectedTasks] = useState([]); @@ -95,7 +95,7 @@ const Run = ({ <ActionButton bg={upstream && 'gray.100'} onClick={onToggleUpstream} name="Upstream" /> <ActionButton bg={downstream && 'gray.100'} onClick={onToggleDownstream} name="Downstream" /> </ButtonGroup> - <Button colorScheme="green" onClick={onClick} isLoading={isMarkLoading || isConfirmLoading}> + <Button colorScheme="green" onClick={onClick} isLoading={isMarkLoading || isConfirmLoading} isDisabled={!!selectedRows.length}> Mark Success </Button> <ConfirmDialog diff --git a/airflow/www/static/js/tree/details/content/taskInstance/taskActions/Run.jsx b/airflow/www/static/js/tree/details/content/taskInstance/taskActions/Run.jsx index cfc649e9de..abb17a8479 100644 --- a/airflow/www/static/js/tree/details/content/taskInstance/taskActions/Run.jsx +++ b/airflow/www/static/js/tree/details/content/taskInstance/taskActions/Run.jsx @@ -35,6 +35,7 @@ const Run = ({ dagId, runId, taskId, + selectedRows, }) => { const containerRef = useContainerRef(); const [ignoreAllDeps, setIgnoreAllDeps] = useState(false); @@ -49,11 +50,22 @@ const Run = ({ const { mutate: onRun, isLoading } = useRunTask(dagId, runId, taskId); const onClick = () => { - onRun({ - ignoreAllDeps, - ignoreTaskState, - ignoreTaskDeps, - }); + if (selectedRows.length) { + selectedRows.forEach((mapIndex) => { + onRun({ + ignoreAllDeps, + ignoreTaskState, + ignoreTaskDeps, + mapIndex, + }); + }); + } else { + onRun({ + ignoreAllDeps, + ignoreTaskState, + ignoreTaskDeps, + }); + } }; return (
