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/09 18:57:48 UTC

[airflow] branch mapped-instance-actions created (now d21397848d)

This is an automated email from the ASF dual-hosted git repository.

bbovenzi pushed a change to branch mapped-instance-actions
in repository https://gitbox.apache.org/repos/asf/airflow.git


      at d21397848d Allow bulk mapped task actions

This branch includes the following new commits:

     new 85f44b97dc Add Xcom button, hide map index actions, disabled run
     new d21397848d Allow bulk mapped task actions

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[airflow] 02/02: Allow bulk mapped task actions

Posted by bb...@apache.org.
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 <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 +++++++++++++++++--
 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 (


[airflow] 01/02: Add Xcom button, hide map index actions, disabled run

Posted by bb...@apache.org.
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 85f44b97dcc3dd46188127d73b4b3f01d08afb4c
Author: Brent Bovenzi <br...@gmail.com>
AuthorDate: Sat Apr 9 13:27:38 2022 -0400

    Add Xcom button, hide map index actions, disabled run
---
 airflow/www/static/js/dag.js                       | 18 +++++++++++++++++
 .../content/taskInstance/MappedInstances.jsx       | 17 +++++++++-------
 .../js/tree/details/content/taskInstance/Nav.jsx   |  9 ++++++++-
 .../content/taskInstance/taskActions/Run.jsx       | 21 ++++++++++++++++----
 airflow/www/templates/airflow/dag.html             | 23 ++++++++++++++--------
 5 files changed, 68 insertions(+), 20 deletions(-)

diff --git a/airflow/www/static/js/dag.js b/airflow/www/static/js/dag.js
index 1606737017..145d1f2666 100644
--- a/airflow/www/static/js/dag.js
+++ b/airflow/www/static/js/dag.js
@@ -124,6 +124,13 @@ function updateModalUrls() {
     execution_date: executionDate,
     map_index: mapIndex,
   });
+
+  updateButtonUrl(buttons.xcom, {
+    dag_id: dagId,
+    task_id: taskId,
+    execution_date: executionDate,
+    map_index: mapIndex,
+  });
 }
 
 // Update modal urls on toggle
@@ -170,9 +177,16 @@ export function callModal({
   if (mi >= 0) {
     $('#modal_map_index').show();
     $('#modal_map_index .value').text(mi);
+    // Marking state and clear are not yet supported for mapped instances
+    $('#success_action').hide();
+    $('#failed_action').hide();
+    $('#clear_action').hide();
   } else {
     $('#modal_map_index').hide();
     $('#modal_map_index .value').text('');
+    $('#success_action').show();
+    $('#failed_action').show();
+    $('#clear_action').show();
   }
   if (isSubDag) {
     $('#div_btn_subdag').show();
@@ -197,8 +211,12 @@ export function callModal({
     $('#btn_mapped').show();
     $('#mapped_dropdown').css('display', 'inline-block');
     $('#btn_rendered').hide();
+    $('#btn_xcom').hide();
+    $('#btn_log').hide();
   } else {
     $('#btn_rendered').show();
+    $('#btn_xcom').show();
+    $('#btn_log').show();
     $('#btn_mapped').hide();
     $('#mapped_dropdown').hide();
   }
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 b815f0987f..42bbdca66f 100644
--- a/airflow/www/static/js/tree/details/content/taskInstance/MappedInstances.jsx
+++ b/airflow/www/static/js/tree/details/content/taskInstance/MappedInstances.jsx
@@ -26,9 +26,9 @@ import {
   IconButton,
 } from '@chakra-ui/react';
 import { snakeCase } from 'lodash';
-import { FaMicroscope } from 'react-icons/fa';
-import { GiLog } from 'react-icons/gi';
-import { HiTemplate } from 'react-icons/hi';
+import {
+  MdDetails, MdCode, MdSyncAlt, MdReorder,
+} from 'react-icons/md';
 
 import { getMetaValue } from '../../../../utils';
 import { formatDateTime, formatDuration } from '../../../../datetime_utils';
@@ -39,9 +39,10 @@ import Table from '../../../Table';
 const renderedTemplatesUrl = getMetaValue('rendered_templates_url');
 const logUrl = getMetaValue('log_url');
 const taskUrl = getMetaValue('task_url');
+const xcomUrl = getMetaValue('xcom_url');
 
 const IconLink = (props) => (
-  <IconButton as={Link} variant="outline" colorScheme="blue" {...props} />
+  <IconButton as={Link} variant="ghost" colorScheme="blue" fontSize="3xl" {...props} />
 );
 
 const MappedInstances = ({
@@ -73,6 +74,7 @@ const MappedInstances = ({
       const detailsLink = `${taskUrl}&${params}`;
       const renderedLink = `${renderedTemplatesUrl}&${params}`;
       const logLink = `${logUrl}&${params}`;
+      const xcomLink = `${xcomUrl}&${params}`;
       return {
         ...mi,
         state: (
@@ -86,9 +88,10 @@ const MappedInstances = ({
         endDate: mi.endDate && formatDateTime(mi.endDate),
         links: (
           <Flex alignItems="center">
-            <IconLink mr={1} title="Rendered Templates" aria-label="Rendered Templates" icon={<HiTemplate />} href={renderedLink} />
-            <IconLink mr={1} title="Log" aria-label="Log" icon={<GiLog />} href={logLink} />
-            <IconLink title="Details" aria-label="Details" icon={<FaMicroscope />} href={detailsLink} />
+            <IconLink mr={1} title="Details" aria-label="Details" icon={<MdDetails />} href={detailsLink} />
+            <IconLink mr={1} title="Rendered Templates" aria-label="Rendered Templates" icon={<MdCode />} href={renderedLink} />
+            <IconLink mr={1} title="Log" aria-label="Log" icon={<MdReorder />} href={logLink} />
+            <IconLink title="XCom" fontWeight="bold" aria-label="XCom" icon={<MdSyncAlt />} href={xcomLink} />
           </Flex>
         ),
       };
diff --git a/airflow/www/static/js/tree/details/content/taskInstance/Nav.jsx b/airflow/www/static/js/tree/details/content/taskInstance/Nav.jsx
index 7550bdb842..1b7062f898 100644
--- a/airflow/www/static/js/tree/details/content/taskInstance/Nav.jsx
+++ b/airflow/www/static/js/tree/details/content/taskInstance/Nav.jsx
@@ -34,12 +34,17 @@ const baseDate = getMetaValue('base_date');
 const taskInstancesUrl = getMetaValue('task_instances_list_url');
 const renderedK8sUrl = getMetaValue('rendered_k8s_url');
 const renderedTemplatesUrl = getMetaValue('rendered_templates_url');
+const xcomUrl = getMetaValue('xcom_url');
 const logUrl = getMetaValue('log_url');
 const taskUrl = getMetaValue('task_url');
 const gridUrl = getMetaValue('grid_url');
 const gridUrlNoRoot = getMetaValue('grid_url_no_root');
 
-const LinkButton = ({ children, ...rest }) => (<Button as={Link} variant="ghost" colorScheme="blue" {...rest}>{children}</Button>);
+const LinkButton = ({ children, ...rest }) => (
+  <Button as={Link} aria-label={children} variant="ghost" colorScheme="blue" {...rest}>
+    {children}
+  </Button>
+);
 
 const Nav = ({
   taskId, executionDate, operator, isMapped,
@@ -51,6 +56,7 @@ const Nav = ({
   const detailsLink = `${taskUrl}&${params}`;
   const renderedLink = `${renderedTemplatesUrl}&${params}`;
   const logLink = `${logUrl}&${params}`;
+  const xcomLink = `${xcomUrl}&${params}`;
   const k8sLink = `${renderedK8sUrl}&${params}`;
   const listParams = new URLSearchParams({
     _flt_3_dag_id: dagId,
@@ -89,6 +95,7 @@ const Nav = ({
           <LinkButton href={subDagLink}>Zoom into SubDag</LinkButton>
           )}
           <LinkButton href={logLink}>Log</LinkButton>
+          <LinkButton href={xcomLink}>XCom</LinkButton>
         </>
         )}
         <LinkButton href={allInstancesLink} title="View all instances across all DAG runs">All Instances</LinkButton>
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..d77c3e947d 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
@@ -22,15 +22,21 @@ import {
   Button,
   Flex,
   ButtonGroup,
+  Tooltip,
 } from '@chakra-ui/react';
 
 import { useRunTask } from '../../../../api';
+import { getMetaValue } from '../../../../../utils';
+import { useContainerRef } from '../../../../context/containerRef';
+
+const canRun = getMetaValue('k8s_or_k8scelery_executor') === 'True';
 
 const Run = ({
   dagId,
   runId,
   taskId,
 }) => {
+  const containerRef = useContainerRef();
   const [ignoreAllDeps, setIgnoreAllDeps] = useState(false);
   const onToggleAllDeps = () => setIgnoreAllDeps(!ignoreAllDeps);
 
@@ -52,7 +58,7 @@ const Run = ({
 
   return (
     <Flex justifyContent="space-between" width="100%">
-      <ButtonGroup isAttached variant="outline">
+      <ButtonGroup isAttached variant="outline" disabled={!canRun}>
         <Button
           bg={ignoreAllDeps && 'gray.100'}
           onClick={onToggleAllDeps}
@@ -75,9 +81,16 @@ const Run = ({
           Ignore Task Deps
         </Button>
       </ButtonGroup>
-      <Button colorScheme="blue" onClick={onClick} isLoading={isLoading}>
-        Run
-      </Button>
+      <Tooltip
+        label="Only works with the Celery, CeleryKubernetes or Kubernetes executors"
+        shouldWrapChildren // Will show the tooltip even if the button is disabled
+        disabled={canRun}
+        portalProps={{ containerRef }}
+      >
+        <Button colorScheme="blue" onClick={onClick} isLoading={isLoading} disabled={!canRun}>
+          Run
+        </Button>
+      </Tooltip>
     </Flex>
   );
 };
diff --git a/airflow/www/templates/airflow/dag.html b/airflow/www/templates/airflow/dag.html
index 16c0d27cb5..277cd53979 100644
--- a/airflow/www/templates/airflow/dag.html
+++ b/airflow/www/templates/airflow/dag.html
@@ -62,6 +62,7 @@
   <meta name="dagrun_details_url" content="{{ url_for('Airflow.dagrun_details', redirect_url=request.base_url, dag_id=dag.dag_id) }}">
   <meta name="task_url" content="{{ url_for('Airflow.task', dag_id=dag.dag_id) }}">
   <meta name="log_url" content="{{ url_for('Airflow.log', dag_id=dag.dag_id) }}">
+  <meta name="xcom_url" content="{{ url_for('Airflow.xcom', dag_id=dag.dag_id) }}">
   <meta name="rendered_templates_url" content="{{ url_for('Airflow.rendered_templates', dag_id=dag.dag_id) }}">
   <meta name="rendered_k8s_url" content="{{ url_for('Airflow.rendered_k8s', dag_id=dag.dag_id) }}">
   <meta name="task_instances_list_url" content="{{ url_for('TaskInstanceModelView.list') }}">
@@ -234,6 +235,9 @@
           <a id="btn_log" class="btn btn-sm" data-base-url="{{ url_for('Airflow.log') }}">
             Log
           </a>
+          <a id="btn_xcom" class="btn btn-sm" data-base-url="{{ url_for('Airflow.xcom') }}">
+            XCom
+          </a>
           <a id="btn_ti" class="btn btn-sm" data-base-url="{{ url_for('TaskInstanceModelView.list') }}" title="View all instances across all DAG runs">
             All Instances
           </a>
@@ -273,7 +277,7 @@
             </div>
           {% endif %}
           <h4>Task Actions</h4>
-          <form method="POST" data-action="{{ url_for('Airflow.run') }}">
+          <form method="POST" data-action="{{ url_for('Airflow.run') }}" id="run_action">
             <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
             <input type="hidden" name="dag_id" value="{{ dag.dag_id }}">
             <input type="hidden" name="task_id">
@@ -281,6 +285,9 @@
             <input type="hidden" name="map_index">
             <input type="hidden" name="origin" value="{{ request.base_url }}">
             <div class="row">
+              <span class="col-xs-12 col-sm-9 text-danger" style="font-size: 12px">
+                {{ "Only works with the Celery, CeleryKubernetes or Kubernetes executors, sorry" if not k8s_or_k8scelery_executor else "" }}
+            </span>
               <span class="btn-group col-xs-12 col-sm-9 task-instance-modal-column" data-toggle="buttons">
                 <label
                   class="btn btn-default"
@@ -299,14 +306,14 @@
                 </label>
               </span>
               <span class="col-xs-12 col-sm-3 task-instance-modal-column">
-                <button type="submit" id="btn_run" class="btn btn-primary btn-block" title="Runs a single task instance">
+                <button type="submit" id="btn_run" class="btn btn-primary btn-block" title="Runs a single task instance" {{ " disabled" if not k8s_or_k8scelery_executor else "" }}>
                   Run
                 </button>
               </span>
             </div>
+            <hr style="margin-bottom: 8px;">
           </form>
-          <hr style="margin-bottom: 8px;">
-          <form method="POST" data-action="{{ url_for('Airflow.clear') }}">
+          <form method="POST" data-action="{{ url_for('Airflow.clear') }}" id="clear_action">
             <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
             <input type="hidden" name="dag_id" value="{{ dag.dag_id }}">
             <input type="hidden" name="task_id">
@@ -346,9 +353,9 @@
                 </button>
               </span>
             </div>
+            <hr style="margin-bottom: 8px;">
           </form>
-          <hr style="margin-bottom: 8px;">
-          <form method="GET" data-action="{{ url_for('Airflow.confirm') }}">
+          <form method="GET" data-action="{{ url_for('Airflow.confirm') }}" id="failed_action">
             <input type="hidden" name="dag_id" value="{{ dag.dag_id }}">
             <input type="hidden" name="task_id">
             <input type="hidden" name="dag_run_id">
@@ -379,9 +386,9 @@
                 </button>
               </span>
             </div>
+            <hr style="margin-bottom: 8px;">
           </form>
-          <hr style="margin-bottom: 8px;">
-          <form method="GET" data-action="{{ url_for('Airflow.confirm') }}">
+          <form method="GET" data-action="{{ url_for('Airflow.confirm') }}" id="success_action">
             <input type="hidden" name="dag_id" value="{{ dag.dag_id }}">
             <input type="hidden" name="task_id">
             <input type="hidden" name="dag_run_id">