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 d21397848d351a2c7067b9fe5cd033b6dfaa9cbf 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 +++++++++++++++++-- airflow/www/static/js/tree/api/useRunTask.js | 2 + .../content/taskInstance/MappedInstances.jsx | 3 +- .../js/tree/details/content/taskInstance/index.jsx | 49 ++++++++++++++++++---- .../content/taskInstance/taskActions/Clear.jsx | 2 + .../taskInstance/taskActions/MarkFailed.jsx | 3 +- .../taskInstance/taskActions/MarkSuccess.jsx | 6 ++- .../content/taskInstance/taskActions/Run.jsx | 22 +++++++--- 8 files changed, 110 insertions(+), 21 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/api/useRunTask.js b/airflow/www/static/js/tree/api/useRunTask.js index f6f402f02c..44a9e14bf4 100644 --- a/airflow/www/static/js/tree/api/useRunTask.js +++ b/airflow/www/static/js/tree/api/useRunTask.js @@ -36,6 +36,7 @@ export default function useRunTask(dagId, runId, taskId) { ignoreAllDeps, ignoreTaskState, ignoreTaskDeps, + mapIndex = -1, }) => { const params = new URLSearchParams({ csrf_token: csrfToken, @@ -45,6 +46,7 @@ export default function useRunTask(dagId, runId, taskId) { ignore_all_deps: ignoreAllDeps, ignore_task_deps: ignoreTaskDeps, ignore_ti_state: ignoreTaskState, + map_index: mapIndex, }).toString(); return axios.post(runUrl, params, { 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 63c957a788..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 && ( @@ -79,20 +87,45 @@ const TaskInstance = ({ taskId, runId }) => { /> )} {!isGroup && ( - <> - <VStack justifyContent="center" divider={<StackDivider my={3} />} my={3}> - <RunAction runId={runId} taskId={taskId} dagId={dagId} /> + <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} /> - <MarkFailedAction runId={runId} taskId={taskId} dagId={dagId} /> - <MarkSuccessAction runId={runId} taskId={taskId} dagId={dagId} /> </VStack> <Divider my={2} /> - </> + </Box> )} {!isMapped && ( <Logs @@ -110,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 e3c56d1f8a..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 @@ -29,7 +29,9 @@ import ConfirmDialog from '../../ConfirmDialog'; import ActionButton from './ActionButton'; import { useMarkSuccessTask, useConfirmMarkTask } from '../../../../api'; -const Run = ({ dagId, runId, taskId }) => { +const Run = ({ + dagId, runId, taskId, selectedRows, +}) => { const [affectedTasks, setAffectedTasks] = useState([]); // Options check/unchecked @@ -93,7 +95,7 @@ const Run = ({ dagId, runId, taskId }) => { <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 d77c3e947d..d161e44531 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 (
