You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by bb...@apache.org on 2022/04/20 14:31:36 UTC
[airflow] 05/09: Allow bulk mapped task actions
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 c633ca889595dee07d8a07defcf4a30d0bac4c82
Author: Brent Bovenzi <br...@gmail.com>
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 aef91ce905..152f647ea3 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 204cec44c2..41c8bb9c6f 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
@@ -30,6 +30,7 @@ const Run = ({
dagId,
runId,
taskId,
+ selectedRows,
}) => {
const [ignoreAllDeps, setIgnoreAllDeps] = useState(false);
const onToggleAllDeps = () => setIgnoreAllDeps(!ignoreAllDeps);
@@ -43,11 +44,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 (