You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by ep...@apache.org on 2022/06/30 14:03:36 UTC

[airflow] 09/14: Migrate jsx files that affect run/task selection to tsx (#24509)

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

ephraimanierobi pushed a commit to branch v2-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 05737bccdaff5851c750049ed919b5ef0e23c473
Author: Brent Bovenzi <br...@gmail.com>
AuthorDate: Wed Jun 22 15:30:24 2022 -0400

    Migrate jsx files that affect run/task selection to tsx (#24509)
    
    * convert all useSelection files to ts
    
    Update grid data ts, remove some anys
    
    * yarn, lint and tests
    
    * convert statusbox to ts
    
    * remove some anys, update instance tooltip
    
    * fix types
    
    * remove any, add comment for global vars
    
    * fix url selection and grid/task defaults
    
    * remove React.FC declarations
    
    * specify tsconfig file path
    
    * remove ts-loader
    
    (cherry picked from commit c3c1f7ea2851377ba913075844a8dde9bfe6376e)
---
 airflow/www/static/js/grid/{Main.jsx => Main.tsx}  |  4 +-
 airflow/www/static/js/grid/ToggleGroups.jsx        |  8 +--
 .../www/static/js/grid/api/{index.js => index.ts}  |  4 +-
 .../js/grid/api/{useGridData.js => useGridData.ts} | 64 ++++++++++--------
 .../js/grid/api/{useTasks.js => useTasks.ts}       | 18 ++++--
 ...ceTooltip.test.jsx => InstanceTooltip.test.tsx} | 22 +++++--
 .../{InstanceTooltip.jsx => InstanceTooltip.tsx}   | 20 ++++--
 .../components/{StatusBox.jsx => StatusBox.tsx}    | 44 +++++++++----
 airflow/www/static/js/grid/components/Time.tsx     |  2 +-
 airflow/www/static/js/grid/context/autorefresh.jsx |  8 ++-
 .../context/{containerRef.jsx => containerRef.tsx} | 11 +++-
 .../js/grid/dagRuns/{index.jsx => index.tsx}       |  8 ++-
 .../useTasks.js => details/BreadcrumbText.tsx}     | 31 +++++----
 .../js/grid/details/{Header.jsx => Header.tsx}     | 25 +++-----
 .../content/taskInstance/{index.jsx => index.tsx}  | 30 ++++++---
 .../js/grid/details/{index.jsx => index.tsx}       | 14 ++--
 .../static/js/grid/{api/useTasks.js => index.d.ts} | 23 +++----
 ...erTaskRows.test.jsx => renderTaskRows.test.tsx} | 21 ++----
 .../{renderTaskRows.jsx => renderTaskRows.tsx}     | 68 +++++++++++++-------
 airflow/www/static/js/grid/types/index.ts          | 75 ++++++++++++++++++++++
 ...useSelection.test.jsx => useSelection.test.tsx} |  8 ++-
 .../utils/{useSelection.js => useSelection.ts}     |  7 +-
 airflow/www/tsconfig.json                          |  4 +-
 airflow/www/webpack.config.js                      |  2 +-
 24 files changed, 344 insertions(+), 177 deletions(-)

diff --git a/airflow/www/static/js/grid/Main.jsx b/airflow/www/static/js/grid/Main.tsx
similarity index 95%
rename from airflow/www/static/js/grid/Main.jsx
rename to airflow/www/static/js/grid/Main.tsx
index 5e9a4f2a9a..8779b547ec 100644
--- a/airflow/www/static/js/grid/Main.jsx
+++ b/airflow/www/static/js/grid/Main.tsx
@@ -47,10 +47,10 @@ const Main = () => {
 
   const onPanelToggle = () => {
     if (!isOpen) {
-      localStorage.setItem(detailsPanelKey, false);
+      localStorage.setItem(detailsPanelKey, 'false');
     } else {
       clearSelection();
-      localStorage.setItem(detailsPanelKey, true);
+      localStorage.setItem(detailsPanelKey, 'true');
     }
     onToggle();
   };
diff --git a/airflow/www/static/js/grid/ToggleGroups.jsx b/airflow/www/static/js/grid/ToggleGroups.jsx
index 3705d67d80..2f027cd668 100644
--- a/airflow/www/static/js/grid/ToggleGroups.jsx
+++ b/airflow/www/static/js/grid/ToggleGroups.jsx
@@ -34,15 +34,15 @@ const getGroupIds = (groups) => {
 };
 
 const ToggleGroups = ({ groups, openGroupIds, onToggleGroups }) => {
+  // Don't show button if the DAG has no task groups
+  const hasGroups = groups.children && groups.children.find((c) => !!c.children);
+  if (!hasGroups) return null;
+
   const allGroupIds = getGroupIds(groups.children);
 
   const isExpandDisabled = allGroupIds.length === openGroupIds.length;
   const isCollapseDisabled = !openGroupIds.length;
 
-  // Don't show button if the DAG has no task groups
-  const hasGroups = groups.children.find((c) => !!c.children);
-  if (!hasGroups) return null;
-
   const onExpand = () => {
     onToggleGroups(allGroupIds);
   };
diff --git a/airflow/www/static/js/grid/api/index.js b/airflow/www/static/js/grid/api/index.ts
similarity index 93%
rename from airflow/www/static/js/grid/api/index.js
rename to airflow/www/static/js/grid/api/index.ts
index 3487ecd6ea..0ac8e4e284 100644
--- a/airflow/www/static/js/grid/api/index.js
+++ b/airflow/www/static/js/grid/api/index.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import axios from 'axios';
+import axios, { AxiosResponse } from 'axios';
 import camelcaseKeys from 'camelcase-keys';
 
 import useTasks from './useTasks';
@@ -35,7 +35,7 @@ import useGridData from './useGridData';
 import useMappedInstances from './useMappedInstances';
 
 axios.interceptors.response.use(
-  (res) => (res.data ? camelcaseKeys(res.data, { deep: true }) : res),
+  (res: AxiosResponse) => (res.data ? camelcaseKeys(res.data, { deep: true }) : res),
 );
 
 axios.defaults.headers.common.Accept = 'application/json';
diff --git a/airflow/www/static/js/grid/api/useGridData.js b/airflow/www/static/js/grid/api/useGridData.ts
similarity index 64%
rename from airflow/www/static/js/grid/api/useGridData.js
rename to airflow/www/static/js/grid/api/useGridData.ts
index 38d4e00748..ec12ee6d60 100644
--- a/airflow/www/static/js/grid/api/useGridData.js
+++ b/airflow/www/static/js/grid/api/useGridData.ts
@@ -17,10 +17,8 @@
  * under the License.
  */
 
-/* global autoRefreshInterval */
-
 import { useQuery } from 'react-query';
-import axios from 'axios';
+import axios, { AxiosResponse } from 'axios';
 
 import { getMetaValue } from '../../utils';
 import { useAutoRefresh } from '../context/autorefresh';
@@ -28,6 +26,7 @@ import useErrorToast from '../utils/useErrorToast';
 import useFilters, {
   BASE_DATE_PARAM, NUM_RUNS_PARAM, RUN_STATE_PARAM, RUN_TYPE_PARAM, now,
 } from '../utils/useFilters';
+import type { Task, DagRun } from '../types';
 
 const DAG_ID_PARAM = 'dag_id';
 
@@ -36,12 +35,21 @@ const dagId = getMetaValue(DAG_ID_PARAM);
 const gridDataUrl = getMetaValue('grid_data_url') || '';
 const urlRoot = getMetaValue('root');
 
-const emptyData = {
+interface GridData {
+  dagRuns: DagRun[];
+  groups: Task;
+}
+
+const emptyGridData: GridData = {
   dagRuns: [],
-  groups: {},
+  groups: {
+    id: null,
+    label: null,
+    instances: [],
+  },
 };
 
-export const areActiveRuns = (runs = []) => runs.filter((run) => ['queued', 'running', 'scheduled'].includes(run.state)).length > 0;
+export const areActiveRuns = (runs: DagRun[] = []) => runs.filter((run) => ['queued', 'running', 'scheduled'].includes(run.state)).length > 0;
 
 const useGridData = () => {
   const { isRefreshOn, stopRefresh } = useAutoRefresh();
@@ -52,8 +60,9 @@ const useGridData = () => {
     },
   } = useFilters();
 
-  return useQuery(['gridData', baseDate, numRuns, runType, runState], async () => {
-    try {
+  const query = useQuery(
+    ['gridData', baseDate, numRuns, runType, runState],
+    async () => {
       const params = {
         root: urlRoot || undefined,
         [DAG_ID_PARAM]: dagId,
@@ -62,24 +71,29 @@ const useGridData = () => {
         [RUN_TYPE_PARAM]: runType,
         [RUN_STATE_PARAM]: runState,
       };
-      const newData = await axios.get(gridDataUrl, { params });
+      const response = await axios.get<AxiosResponse, GridData>(gridDataUrl, { params });
       // turn off auto refresh if there are no active runs
-      if (!areActiveRuns(newData.dagRuns)) stopRefresh();
-      return newData;
-    } catch (error) {
-      stopRefresh();
-      errorToast({
-        title: 'Auto-refresh Error',
-        error,
-      });
-      throw (error);
-    }
-  }, {
-    placeholderData: emptyData,
-    // only refetch if the refresh switch is on
-    refetchInterval: isRefreshOn && autoRefreshInterval * 1000,
-    keepPreviousData: true,
-  });
+      if (!areActiveRuns(response.dagRuns)) stopRefresh();
+      return response;
+    },
+    {
+      // only refetch if the refresh switch is on
+      refetchInterval: isRefreshOn && (autoRefreshInterval || 1) * 1000,
+      keepPreviousData: true,
+      onError: (error) => {
+        stopRefresh();
+        errorToast({
+          title: 'Auto-refresh Error',
+          error,
+        });
+        throw (error);
+      },
+    },
+  );
+  return {
+    ...query,
+    data: query.data ?? emptyGridData,
+  };
 };
 
 export default useGridData;
diff --git a/airflow/www/static/js/grid/api/useTasks.js b/airflow/www/static/js/grid/api/useTasks.ts
similarity index 77%
copy from airflow/www/static/js/grid/api/useTasks.js
copy to airflow/www/static/js/grid/api/useTasks.ts
index c214444dcb..68878a78a0 100644
--- a/airflow/www/static/js/grid/api/useTasks.js
+++ b/airflow/www/static/js/grid/api/useTasks.ts
@@ -17,19 +17,25 @@
  * under the License.
  */
 
-import axios from 'axios';
+import axios, { AxiosResponse } from 'axios';
 import { useQuery } from 'react-query';
 import { getMetaValue } from '../../utils';
 
+interface TaskData {
+  tasks: any[];
+  totalEntries: number;
+}
+
 export default function useTasks() {
-  return useQuery(
+  const query = useQuery<TaskData>(
     'tasks',
     () => {
       const tasksUrl = getMetaValue('tasks_api');
-      return axios.get(tasksUrl);
-    },
-    {
-      initialData: { tasks: [], totalEntries: 0 },
+      return axios.get<AxiosResponse, TaskData>(tasksUrl || '');
     },
   );
+  return {
+    ...query,
+    data: query.data || { tasks: [], totalEntries: 0 },
+  };
 }
diff --git a/airflow/www/static/js/grid/components/InstanceTooltip.test.jsx b/airflow/www/static/js/grid/components/InstanceTooltip.test.tsx
similarity index 80%
rename from airflow/www/static/js/grid/components/InstanceTooltip.test.jsx
rename to airflow/www/static/js/grid/components/InstanceTooltip.test.tsx
index eb1abe8ba4..71e1147da9 100644
--- a/airflow/www/static/js/grid/components/InstanceTooltip.test.jsx
+++ b/airflow/www/static/js/grid/components/InstanceTooltip.test.tsx
@@ -24,19 +24,21 @@ import { render } from '@testing-library/react';
 
 import InstanceTooltip from './InstanceTooltip';
 import { Wrapper } from '../utils/testUtils';
+import type { TaskState } from '../types';
 
 const instance = {
-  startDate: new Date(),
-  endDate: new Date(),
-  state: 'success',
+  startDate: new Date().toISOString(),
+  endDate: new Date().toISOString(),
+  state: 'success' as TaskState,
   runId: 'run',
+  taskId: 'task',
 };
 
 describe('Test Task InstanceTooltip', () => {
   test('Displays a normal task', () => {
     const { getByText } = render(
       <InstanceTooltip
-        group={{}}
+        group={{ id: 'task', label: 'task', instances: [] }}
         instance={instance}
       />,
       { wrapper: Wrapper },
@@ -48,7 +50,9 @@ describe('Test Task InstanceTooltip', () => {
   test('Displays a mapped task with overall status', () => {
     const { getByText } = render(
       <InstanceTooltip
-        group={{ isMapped: true }}
+        group={{
+          id: 'task', label: 'task', instances: [], isMapped: true,
+        }}
         instance={{ ...instance, mappedStates: { success: 2 } }}
       />,
       { wrapper: Wrapper },
@@ -63,12 +67,20 @@ describe('Test Task InstanceTooltip', () => {
     const { getByText, queryByText } = render(
       <InstanceTooltip
         group={{
+          id: 'task',
+          label: 'task',
+          instances: [],
           children: [
             {
+              id: 'child_task',
+              label: 'child_task',
               instances: [
                 {
+                  taskId: 'child_task',
                   runId: 'run',
                   state: 'success',
+                  startDate: '',
+                  endDate: '',
                 },
               ],
             },
diff --git a/airflow/www/static/js/grid/components/InstanceTooltip.jsx b/airflow/www/static/js/grid/components/InstanceTooltip.tsx
similarity index 86%
rename from airflow/www/static/js/grid/components/InstanceTooltip.jsx
rename to airflow/www/static/js/grid/components/InstanceTooltip.tsx
index 8898f5af53..dc15fe1261 100644
--- a/airflow/www/static/js/grid/components/InstanceTooltip.jsx
+++ b/airflow/www/static/js/grid/components/InstanceTooltip.tsx
@@ -23,25 +23,33 @@ import { Box, Text } from '@chakra-ui/react';
 import { finalStatesMap } from '../../utils';
 import { formatDuration, getDuration } from '../../datetime_utils';
 import Time from './Time';
+import type { TaskInstance, Task } from '../types';
+
+interface Props {
+  group: Task;
+  instance: TaskInstance;
+}
 
 const InstanceTooltip = ({
   group,
   instance: {
     startDate, endDate, state, runId, mappedStates,
   },
-}) => {
+}: Props) => {
+  if (!group) return null;
   const isGroup = !!group.children;
-  const { isMapped } = group;
-  const summary = [];
+  const summary: React.ReactNode[] = [];
+
+  const isMapped = group?.isMapped;
 
   const numMap = finalStatesMap();
   let numMapped = 0;
-  if (isGroup) {
+  if (isGroup && group.children) {
     group.children.forEach((child) => {
       const taskInstance = child.instances.find((ti) => ti.runId === runId);
       if (taskInstance) {
         const stateKey = taskInstance.state == null ? 'no_status' : taskInstance.state;
-        if (numMap.has(stateKey)) numMap.set(stateKey, numMap.get(stateKey) + 1);
+        if (numMap.has(stateKey)) numMap.set(stateKey, (numMap.get(stateKey) || 0) + 1);
       }
     });
   } else if (isMapped && mappedStates) {
@@ -88,7 +96,7 @@ const InstanceTooltip = ({
       <Text>
         Started:
         {' '}
-        <Time dateTime={startDate} />
+        <Time dateTime={startDate || ''} />
       </Text>
       <Text>
         Duration:
diff --git a/airflow/www/static/js/grid/components/StatusBox.jsx b/airflow/www/static/js/grid/components/StatusBox.tsx
similarity index 71%
rename from airflow/www/static/js/grid/components/StatusBox.jsx
rename to airflow/www/static/js/grid/components/StatusBox.tsx
index 2f079949a9..f9acf57402 100644
--- a/airflow/www/static/js/grid/components/StatusBox.jsx
+++ b/airflow/www/static/js/grid/components/StatusBox.tsx
@@ -17,36 +17,48 @@
  * under the License.
  */
 
-/* global stateColors */
-
 import React from 'react';
 import { isEqual } from 'lodash';
 import {
   Box,
   useTheme,
+  BoxProps,
 } from '@chakra-ui/react';
 
 import Tooltip from './Tooltip';
 import InstanceTooltip from './InstanceTooltip';
 import { useContainerRef } from '../context/containerRef';
+import type { Task, TaskInstance, TaskState } from '../types';
+import type { SelectionProps } from '../utils/useSelection';
 
 export const boxSize = 10;
 export const boxSizePx = `${boxSize}px`;
 
-export const SimpleStatus = ({ state, ...rest }) => (
+interface SimpleStatusProps extends BoxProps {
+  state: TaskState;
+}
+
+export const SimpleStatus = ({ state, ...rest }: SimpleStatusProps) => (
   <Box
     width={boxSizePx}
     height={boxSizePx}
-    backgroundColor={stateColors[state] || 'white'}
+    backgroundColor={state && stateColors[state] ? stateColors[state] : 'white'}
     borderRadius="2px"
     borderWidth={state ? 0 : 1}
     {...rest}
   />
 );
 
+interface Props {
+  group: Task;
+  instance: TaskInstance;
+  onSelect: (selection: SelectionProps) => void;
+  isActive: boolean;
+}
+
 const StatusBox = ({
   group, instance, onSelect, isActive,
-}) => {
+}: Props) => {
   const containerRef = useContainerRef();
   const { runId, taskId } = instance;
   const { colors } = useTheme();
@@ -54,15 +66,19 @@ const StatusBox = ({
 
   // Fetch the corresponding column element and set its background color when hovering
   const onMouseEnter = () => {
-    [...containerRef.current.getElementsByClassName(`js-${runId}`)]
-      .forEach((e) => {
-        // Don't apply hover if it is already selected
-        if (e.getAttribute('data-selected') === 'false') e.style.backgroundColor = hoverBlue;
-      });
+    if (containerRef && containerRef.current) {
+      ([...containerRef.current.getElementsByClassName(`js-${runId}`)] as HTMLElement[])
+        .forEach((e) => {
+          // Don't apply hover if it is already selected
+          if (e.getAttribute('data-selected') === 'false') e.style.backgroundColor = hoverBlue;
+        });
+    }
   };
   const onMouseLeave = () => {
-    [...containerRef.current.getElementsByClassName(`js-${runId}`)]
-      .forEach((e) => { e.style.backgroundColor = null; });
+    if (containerRef && containerRef.current) {
+      ([...containerRef.current.getElementsByClassName(`js-${runId}`)] as HTMLElement[])
+        .forEach((e) => { e.style.backgroundColor = ''; });
+    }
   };
 
   const onClick = () => {
@@ -97,8 +113,8 @@ const StatusBox = ({
 // The default equality function is a shallow comparison and json objects will return false
 // This custom compare function allows us to do a deeper comparison
 const compareProps = (
-  prevProps,
-  nextProps,
+  prevProps: Props,
+  nextProps: Props,
 ) => (
   isEqual(prevProps.group, nextProps.group)
   && isEqual(prevProps.instance, nextProps.instance)
diff --git a/airflow/www/static/js/grid/components/Time.tsx b/airflow/www/static/js/grid/components/Time.tsx
index 5712d163c9..fbc0b16e79 100644
--- a/airflow/www/static/js/grid/components/Time.tsx
+++ b/airflow/www/static/js/grid/components/Time.tsx
@@ -27,7 +27,7 @@ interface Props {
   format?: string;
 }
 
-const Time: React.FC<Props> = ({ dateTime, format = defaultFormatWithTZ }) => {
+const Time = ({ dateTime, format = defaultFormatWithTZ }: Props) => {
   const { timezone } = useTimezone();
   const time = moment(dateTime);
 
diff --git a/airflow/www/static/js/grid/context/autorefresh.jsx b/airflow/www/static/js/grid/context/autorefresh.jsx
index 35df9b7daf..11c987fe34 100644
--- a/airflow/www/static/js/grid/context/autorefresh.jsx
+++ b/airflow/www/static/js/grid/context/autorefresh.jsx
@@ -29,7 +29,13 @@ const autoRefreshKey = 'disabledAutoRefresh';
 const initialIsPaused = getMetaValue('is_paused') === 'True';
 const isRefreshDisabled = JSON.parse(localStorage.getItem(autoRefreshKey));
 
-const AutoRefreshContext = React.createContext(null);
+const AutoRefreshContext = React.createContext({
+  isRefreshOn: false,
+  isPaused: true,
+  toggleRefresh: () => {},
+  stopRefresh: () => {},
+  startRefresh: () => {},
+});
 
 export const AutoRefreshProvider = ({ children }) => {
   const [isPaused, setIsPaused] = useState(initialIsPaused);
diff --git a/airflow/www/static/js/grid/context/containerRef.jsx b/airflow/www/static/js/grid/context/containerRef.tsx
similarity index 80%
rename from airflow/www/static/js/grid/context/containerRef.jsx
rename to airflow/www/static/js/grid/context/containerRef.tsx
index 9062f907ed..4ddc036428 100644
--- a/airflow/www/static/js/grid/context/containerRef.jsx
+++ b/airflow/www/static/js/grid/context/containerRef.tsx
@@ -19,12 +19,17 @@
 
 import React, { useContext, useRef } from 'react';
 
-const ContainerRefContext = React.createContext(null);
+// eslint-disable-next-line max-len
+const ContainerRefContext = React.createContext<React.RefObject<HTMLDivElement> | undefined>(undefined);
+
+interface Props {
+  children: React.ReactNode;
+}
 
 // containerRef is necessary to render for tooltips, modals, and dialogs
 // This provider allows the containerRef to be accessed by any react component
-export const ContainerRefProvider = ({ children }) => {
-  const containerRef = useRef();
+export const ContainerRefProvider = ({ children }: Props) => {
+  const containerRef = useRef<HTMLDivElement>(null);
 
   return (
     <ContainerRefContext.Provider value={containerRef}>
diff --git a/airflow/www/static/js/grid/dagRuns/index.jsx b/airflow/www/static/js/grid/dagRuns/index.tsx
similarity index 94%
rename from airflow/www/static/js/grid/dagRuns/index.jsx
rename to airflow/www/static/js/grid/dagRuns/index.tsx
index ec588313c6..679b7db3a5 100644
--- a/airflow/www/static/js/grid/dagRuns/index.jsx
+++ b/airflow/www/static/js/grid/dagRuns/index.tsx
@@ -24,14 +24,16 @@ import {
   Text,
   Box,
   Flex,
+  TextProps,
 } from '@chakra-ui/react';
 
 import { useGridData } from '../api';
 import DagRunBar from './Bar';
 import { getDuration, formatDuration } from '../../datetime_utils';
 import useSelection from '../utils/useSelection';
+import type { DagRun } from '../types';
 
-const DurationTick = ({ children, ...rest }) => (
+const DurationTick = ({ children, ...rest }: TextProps) => (
   <Text fontSize="sm" color="gray.400" right={1} position="absolute" whiteSpace="nowrap" {...rest}>
     {children}
   </Text>
@@ -40,7 +42,7 @@ const DurationTick = ({ children, ...rest }) => (
 const DagRuns = () => {
   const { data: { dagRuns } } = useGridData();
   const { selected, onSelect } = useSelection();
-  const durations = [];
+  const durations: number[] = [];
   const runs = dagRuns.map((dagRun) => {
     const duration = getDuration(dagRun.startDate, dagRun.endDate);
     durations.push(duration);
@@ -91,7 +93,7 @@ const DagRuns = () => {
       </Td>
       <Td p={0} align="right" verticalAlign="bottom" borderBottom={0} width={`${runs.length * 16}px`}>
         <Flex justifyContent="flex-end">
-          {runs.map((run, i) => (
+          {runs.map((run: DagRun, i: number) => (
             <DagRunBar
               key={run.runId}
               run={run}
diff --git a/airflow/www/static/js/grid/api/useTasks.js b/airflow/www/static/js/grid/details/BreadcrumbText.tsx
similarity index 66%
copy from airflow/www/static/js/grid/api/useTasks.js
copy to airflow/www/static/js/grid/details/BreadcrumbText.tsx
index c214444dcb..85d5f6dec5 100644
--- a/airflow/www/static/js/grid/api/useTasks.js
+++ b/airflow/www/static/js/grid/details/BreadcrumbText.tsx
@@ -17,19 +17,22 @@
  * under the License.
  */
 
-import axios from 'axios';
-import { useQuery } from 'react-query';
-import { getMetaValue } from '../../utils';
+import React from 'react';
+import {
+  Box,
+  Heading,
+} from '@chakra-ui/react';
 
-export default function useTasks() {
-  return useQuery(
-    'tasks',
-    () => {
-      const tasksUrl = getMetaValue('tasks_api');
-      return axios.get(tasksUrl);
-    },
-    {
-      initialData: { tasks: [], totalEntries: 0 },
-    },
-  );
+interface Props {
+  label: string;
+  value: React.ReactNode;
 }
+
+const BreadcrumbText = ({ label, value }: Props) => (
+  <Box position="relative">
+    <Heading as="h5" size="sm" color="gray.300" position="absolute" top="-12px">{label}</Heading>
+    <Heading as="h3" size="md">{value}</Heading>
+  </Box>
+);
+
+export default BreadcrumbText;
diff --git a/airflow/www/static/js/grid/details/Header.jsx b/airflow/www/static/js/grid/details/Header.tsx
similarity index 86%
rename from airflow/www/static/js/grid/details/Header.jsx
rename to airflow/www/static/js/grid/details/Header.tsx
index c158deabca..db6a75b993 100644
--- a/airflow/www/static/js/grid/details/Header.jsx
+++ b/airflow/www/static/js/grid/details/Header.tsx
@@ -22,8 +22,6 @@ import {
   Breadcrumb,
   BreadcrumbItem,
   BreadcrumbLink,
-  Box,
-  Heading,
   Text,
 } from '@chakra-ui/react';
 import { MdPlayArrow, MdOutlineSchedule } from 'react-icons/md';
@@ -33,20 +31,15 @@ import { getMetaValue } from '../../utils';
 import useSelection from '../utils/useSelection';
 import Time from '../components/Time';
 import { useTasks, useGridData } from '../api';
+import BreadcrumbText from './BreadcrumbText';
 
 const dagId = getMetaValue('dag_id');
 
-const LabelValue = ({ label, value }) => (
-  <Box position="relative">
-    <Heading as="h5" size="sm" color="gray.300" position="absolute" top="-12px">{label}</Heading>
-    <Heading as="h3" size="md">{value}</Heading>
-  </Box>
-);
-
 const Header = () => {
   const { data: { dagRuns } } = useGridData();
-  const { selected: { taskId, runId }, onSelect, clearSelection } = useSelection();
   const { data: { tasks } } = useTasks();
+
+  const { selected: { taskId, runId }, onSelect, clearSelection } = useSelection();
   const dagRun = dagRuns.find((r) => r.runId === runId);
   const task = tasks.find((t) => t.taskId === taskId);
 
@@ -59,7 +52,7 @@ const Header = () => {
   }, [clearSelection, dagRun, runId]);
 
   let runLabel;
-  if (dagRun) {
+  if (dagRun && runId) {
     if (runId.includes('manual__') || runId.includes('scheduled__') || runId.includes('backfill__')) {
       runLabel = (<Time dateTime={dagRun.dataIntervalStart || dagRun.executionDate} />);
     } else {
@@ -91,30 +84,30 @@ const Header = () => {
 
   const isMapped = task && task.isMapped;
   const lastIndex = taskId ? taskId.lastIndexOf('.') : null;
-  const taskName = lastIndex ? taskId.substring(lastIndex + 1) : taskId;
+  const taskName = taskId && lastIndex ? taskId.substring(lastIndex + 1) : taskId;
 
   const isDagDetails = !runId && !taskId;
-  const isRunDetails = runId && !taskId;
+  const isRunDetails = !!(runId && !taskId);
   const isTaskDetails = runId && taskId;
 
   return (
     <Breadcrumb separator={<Text color="gray.300">/</Text>}>
       <BreadcrumbItem isCurrentPage={isDagDetails} mt={4}>
         <BreadcrumbLink onClick={clearSelection} _hover={isDagDetails ? { cursor: 'default' } : undefined}>
-          <LabelValue label="DAG" value={dagId} />
+          <BreadcrumbText label="DAG" value={dagId} />
         </BreadcrumbLink>
       </BreadcrumbItem>
       {runId && (
         <BreadcrumbItem isCurrentPage={isRunDetails} mt={4}>
           <BreadcrumbLink onClick={() => onSelect({ runId })} _hover={isRunDetails ? { cursor: 'default' } : undefined}>
-            <LabelValue label="Run" value={runLabel} />
+            <BreadcrumbText label="Run" value={runLabel} />
           </BreadcrumbLink>
         </BreadcrumbItem>
       )}
       {taskId && (
         <BreadcrumbItem isCurrentPage mt={4}>
           <BreadcrumbLink _hover={isTaskDetails ? { cursor: 'default' } : undefined}>
-            <LabelValue label="Task" value={`${taskName}${isMapped ? ' []' : ''}`} />
+            <BreadcrumbText label="Task" value={`${taskName}${isMapped ? ' []' : ''}`} />
           </BreadcrumbLink>
         </BreadcrumbItem>
       )}
diff --git a/airflow/www/static/js/grid/details/content/taskInstance/index.jsx b/airflow/www/static/js/grid/details/content/taskInstance/index.tsx
similarity index 85%
rename from airflow/www/static/js/grid/details/content/taskInstance/index.jsx
rename to airflow/www/static/js/grid/details/content/taskInstance/index.tsx
index 90ef2e839e..43254fd66b 100644
--- a/airflow/www/static/js/grid/details/content/taskInstance/index.jsx
+++ b/airflow/www/static/js/grid/details/content/taskInstance/index.tsx
@@ -38,10 +38,20 @@ import Details from './Details';
 import { useGridData, useTasks } from '../../../api';
 import MappedInstances from './MappedInstances';
 import { getMetaValue } from '../../../../utils';
+import type { Task, DagRun } from '../../../types';
 
 const dagId = getMetaValue('dag_id');
 
-const getTask = ({ taskId, runId, task }) => {
+interface Props {
+  taskId: Task['id'];
+  runId: DagRun['runId'];
+}
+
+interface GetTaskProps extends Props {
+  task: Task;
+}
+
+const getTask = ({ taskId, runId, task }: GetTaskProps) => {
   if (task.id === taskId) return task;
   if (task.children) {
     let foundTask;
@@ -54,10 +64,10 @@ const getTask = ({ taskId, runId, task }) => {
   return null;
 };
 
-const TaskInstance = ({ taskId, runId }) => {
+const TaskInstance = ({ taskId, runId }: Props) => {
   const [selectedRows, setSelectedRows] = useState([]);
-  const { data: { groups, dagRuns } } = useGridData();
-  const { data: { tasks } } = useTasks(dagId);
+  const { data: { dagRuns, groups } } = useGridData();
+  const { data: { tasks } } = useTasks();
 
   const group = getTask({ taskId, runId, task: groups });
   const run = dagRuns.find((r) => r.runId === runId);
@@ -65,11 +75,11 @@ const TaskInstance = ({ taskId, runId }) => {
   if (!group || !run) return null;
 
   const { executionDate } = run;
-  const task = tasks.find((t) => t.taskId === taskId);
-  const operator = task && task.classRef && task.classRef.className ? task.classRef.className : '';
+  const task: any = tasks.find((t: any) => t.taskId === taskId);
+  const operator = (task?.classRef && task?.classRef?.className) ?? '';
 
-  const isGroup = !!group.children;
-  const { isMapped, extraLinks } = group;
+  const isGroup = !!group?.children;
+  const isMapped = !!group?.isMapped;
 
   const instance = group.instances.find((ti) => ti.runId === runId);
 
@@ -128,7 +138,7 @@ const TaskInstance = ({ taskId, runId }) => {
           dagId={dagId}
           taskId={taskId}
           executionDate={executionDate}
-          tryNumber={instance.tryNumber}
+          tryNumber={instance?.tryNumber}
         />
       )}
       <Details instance={instance} group={group} operator={operator} />
@@ -136,7 +146,7 @@ const TaskInstance = ({ taskId, runId }) => {
         taskId={taskId}
         dagId={dagId}
         executionDate={executionDate}
-        extraLinks={extraLinks}
+        extraLinks={group?.extraLinks || []}
       />
       {isMapped && (
         <MappedInstances
diff --git a/airflow/www/static/js/grid/details/index.jsx b/airflow/www/static/js/grid/details/index.tsx
similarity index 82%
rename from airflow/www/static/js/grid/details/index.jsx
rename to airflow/www/static/js/grid/details/index.tsx
index a5a3a57e8b..4c80fd6559 100644
--- a/airflow/www/static/js/grid/details/index.jsx
+++ b/airflow/www/static/js/grid/details/index.tsx
@@ -31,20 +31,20 @@ import DagContent from './content/Dag';
 import useSelection from '../utils/useSelection';
 
 const Details = () => {
-  const { selected } = useSelection();
+  const { selected: { runId, taskId } } = useSelection();
   return (
     <Flex flexDirection="column" pl={3} mr={3} flexGrow={1} maxWidth="750px">
       <Header />
       <Divider my={2} />
       <Box minWidth="750px">
-        {!selected.runId && !selected.taskId && <DagContent />}
-        {selected.runId && !selected.taskId && (
-          <DagRunContent runId={selected.runId} />
+        {!runId && !taskId && <DagContent />}
+        {runId && !taskId && (
+          <DagRunContent runId={runId} />
         )}
-        {selected.taskId && (
+        {taskId && runId && (
         <TaskInstanceContent
-          runId={selected.runId}
-          taskId={selected.taskId}
+          runId={runId}
+          taskId={taskId}
         />
         )}
       </Box>
diff --git a/airflow/www/static/js/grid/api/useTasks.js b/airflow/www/static/js/grid/index.d.ts
similarity index 69%
rename from airflow/www/static/js/grid/api/useTasks.js
rename to airflow/www/static/js/grid/index.d.ts
index c214444dcb..174b4e5e51 100644
--- a/airflow/www/static/js/grid/api/useTasks.js
+++ b/airflow/www/static/js/grid/index.d.ts
@@ -17,19 +17,12 @@
  * under the License.
  */
 
-import axios from 'axios';
-import { useQuery } from 'react-query';
-import { getMetaValue } from '../../utils';
-
-export default function useTasks() {
-  return useQuery(
-    'tasks',
-    () => {
-      const tasksUrl = getMetaValue('tasks_api');
-      return axios.get(tasksUrl);
-    },
-    {
-      initialData: { tasks: [], totalEntries: 0 },
-    },
-  );
+// define global variables that come from FAB
+declare global {
+  const autoRefreshInterval: number | undefined;
+  const stateColors: {
+    [key: string]: string;
+  };
 }
+
+export {};
diff --git a/airflow/www/static/js/grid/renderTaskRows.test.jsx b/airflow/www/static/js/grid/renderTaskRows.test.tsx
similarity index 87%
rename from airflow/www/static/js/grid/renderTaskRows.test.jsx
rename to airflow/www/static/js/grid/renderTaskRows.test.tsx
index 7b1596512a..4f5fabc920 100644
--- a/airflow/www/static/js/grid/renderTaskRows.test.jsx
+++ b/airflow/www/static/js/grid/renderTaskRows.test.tsx
@@ -24,10 +24,11 @@ import { render } from '@testing-library/react';
 
 import renderTaskRows from './renderTaskRows';
 import { TableWrapper } from './utils/testUtils';
+import type { Task } from './types';
 
 describe('Test renderTaskRows', () => {
   test('Renders name and task instance', () => {
-    const task = {
+    const task: Task = {
       id: null,
       label: null,
       children: [
@@ -37,16 +38,11 @@ describe('Test renderTaskRows', () => {
           label: 'group_1',
           instances: [
             {
-              dagId: 'dagId',
-              duration: 0,
               endDate: '2021-10-26T15:42:03.391939+00:00',
-              executionDate: '2021-10-25T15:41:09.726436+00:00',
-              operator: 'DummyOperator',
               runId: 'run1',
               startDate: '2021-10-26T15:42:03.391917+00:00',
               state: 'success',
               taskId: 'group_1',
-              tryNumber: 1,
             },
           ],
           children: [
@@ -56,16 +52,11 @@ describe('Test renderTaskRows', () => {
               extraLinks: [],
               instances: [
                 {
-                  dagId: 'dagId',
-                  duration: 0,
                   endDate: '2021-10-26T15:42:03.391939+00:00',
-                  executionDate: '2021-10-25T15:41:09.726436+00:00',
-                  operator: 'DummyOperator',
                   runId: 'run1',
                   startDate: '2021-10-26T15:42:03.391917+00:00',
                   state: 'success',
                   taskId: 'group_1.task_1',
-                  tryNumber: 1,
                 },
               ],
             },
@@ -110,7 +101,7 @@ describe('Test renderTaskRows', () => {
   });
 
   test('Still renders correctly if task instance is null', () => {
-    const task = {
+    const task: Task = {
       id: null,
       label: null,
       children: [
@@ -118,18 +109,18 @@ describe('Test renderTaskRows', () => {
           extraLinks: [],
           id: 'group_1',
           label: 'group_1',
-          instances: [null],
+          instances: [],
           children: [
             {
               id: 'group_1.task_1',
               label: 'group_1.task_1',
               extraLinks: [],
-              instances: [null],
+              instances: [],
             },
           ],
         },
       ],
-      instances: [null],
+      instances: [],
     };
 
     const { queryByTestId, getByText } = render(
diff --git a/airflow/www/static/js/grid/renderTaskRows.jsx b/airflow/www/static/js/grid/renderTaskRows.tsx
similarity index 75%
rename from airflow/www/static/js/grid/renderTaskRows.jsx
rename to airflow/www/static/js/grid/renderTaskRows.tsx
index 87f93d904e..c7f82333f4 100644
--- a/airflow/www/static/js/grid/renderTaskRows.jsx
+++ b/airflow/www/static/js/grid/renderTaskRows.tsx
@@ -30,30 +30,54 @@ import {
 import StatusBox, { boxSize, boxSizePx } from './components/StatusBox';
 import TaskName from './components/TaskName';
 
-import useSelection from './utils/useSelection';
+import useSelection, { SelectionProps } from './utils/useSelection';
+import type { Task, DagRun } from './types';
 
 const boxPadding = 3;
 const boxPaddingPx = `${boxPadding}px`;
 const columnWidth = boxSize + 2 * boxPadding;
 
+interface RowProps {
+  task: Task;
+  dagRunIds: DagRun['runId'][];
+  level?: number;
+  openParentCount?: number;
+  openGroupIds?: string[];
+  onToggleGroups?: (groupIds: string[]) => void;
+  hoveredTaskState?: string;
+}
+
 const renderTaskRows = ({
   task, level = 0, ...rest
-}) => task.children && task.children.map((t) => (
-  <Row
-    {...rest}
-    key={t.id}
-    task={t}
-    level={level}
-  />
-));
+}: RowProps) => (
+  <>
+    {(task?.children || []).map((t) => (
+      // eslint-disable-next-line @typescript-eslint/no-use-before-define
+      <Row
+        {...rest}
+        key={t.id}
+        task={t}
+        level={level}
+      />
+    ))}
+  </>
+);
+
+interface TaskInstancesProps {
+  task: Task;
+  dagRunIds: string[];
+  selectedRunId?: string | null;
+  onSelect: (selection: SelectionProps) => void;
+  hoveredTaskState?: string;
+}
 
 const TaskInstances = ({
-  task, dagRunIds, selectedRunId, onSelect, activeTaskState,
-}) => (
+  task, dagRunIds, selectedRunId, onSelect, hoveredTaskState,
+}: TaskInstancesProps) => (
   <Flex justifyContent="flex-end">
-    {dagRunIds.map((runId) => {
+    {dagRunIds.map((runId: string) => {
       // Check if an instance exists for the run, or return an empty box
-      const instance = task.instances.find((gi) => gi && gi.runId === runId);
+      const instance = task.instances.find((ti) => ti && ti.runId === runId);
       const isSelected = selectedRunId === runId;
       return (
         <Box
@@ -63,7 +87,7 @@ const TaskInstances = ({
           data-selected={isSelected}
           transition="background-color 0.2s"
           key={`${runId}-${task.id}`}
-          bg={isSelected && 'blue.100'}
+          bg={isSelected ? 'blue.100' : undefined}
         >
           {instance
             ? (
@@ -71,7 +95,7 @@ const TaskInstances = ({
                 instance={instance}
                 group={task}
                 onSelect={onSelect}
-                isActive={activeTaskState === undefined || activeTaskState === instance.state}
+                isActive={hoveredTaskState === undefined || hoveredTaskState === instance.state}
               />
             )
             : <Box width={boxSizePx} data-testid="blank-task" />}
@@ -81,10 +105,10 @@ const TaskInstances = ({
   </Flex>
 );
 
-const Row = (props) => {
+const Row = (props: RowProps) => {
   const {
     task,
-    level,
+    level = 0,
     dagRunIds,
     openParentCount = 0,
     openGroupIds = [],
@@ -103,7 +127,7 @@ const Row = (props) => {
   // assure the function is the same across renders
   const memoizedToggle = useCallback(
     () => {
-      if (isGroup) {
+      if (isGroup && task.label) {
         let newGroupIds = [];
         if (!isOpen) {
           newGroupIds = [...openGroupIds, task.label];
@@ -121,16 +145,16 @@ const Row = (props) => {
   return (
     <>
       <Tr
-        bg={isSelected && 'blue.100'}
+        bg={isSelected ? 'blue.100' : 'inherit'}
         borderBottomWidth={isFullyOpen ? 1 : 0}
         borderBottomColor={isGroup && isOpen ? 'gray.400' : 'gray.200'}
         role="group"
-        _hover={!isSelected && { bg: hoverBlue }}
+        _hover={!isSelected ? { bg: hoverBlue } : undefined}
         transition="background-color 0.2s"
       >
         <Td
           bg={isSelected ? 'blue.100' : 'white'}
-          _groupHover={!isSelected && ({ bg: 'blue.50' })}
+          _groupHover={!isSelected ? { bg: 'blue.50' } : undefined}
           p={0}
           transition="background-color 0.2s"
           lineHeight="18px"
@@ -164,7 +188,7 @@ const Row = (props) => {
               task={task}
               selectedRunId={selected.runId}
               onSelect={onSelect}
-              activeTaskState={hoveredTaskState}
+              hoveredTaskState={hoveredTaskState}
             />
           </Collapse>
         </Td>
diff --git a/airflow/www/static/js/grid/types/index.ts b/airflow/www/static/js/grid/types/index.ts
new file mode 100644
index 0000000000..df1f873ef7
--- /dev/null
+++ b/airflow/www/static/js/grid/types/index.ts
@@ -0,0 +1,75 @@
+/*!
+ * 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.
+ */
+
+type RunState = 'success' | 'running' | 'queued' | 'failed';
+
+type TaskState = RunState
+| 'removed'
+| 'scheduled'
+| 'shutdown'
+| 'restarting'
+| 'up_for_retry'
+| 'up_for_reschedule'
+| 'upstream_failed'
+| 'skipped'
+| 'sensing'
+| 'deferred'
+| null;
+
+interface DagRun {
+  runId: string;
+  runType: 'manual' | 'backfill' | 'scheduled';
+  state: RunState;
+  executionDate: string;
+  dataIntervalStart: string;
+  dataIntervalEnd: string;
+  startDate: string | null;
+  endDate: string | null;
+  lastSchedulingDecision: string | null;
+}
+
+interface TaskInstance {
+  runId: string;
+  taskId: string;
+  startDate: string | null;
+  endDate: string | null;
+  state: TaskState | null;
+  mappedStates?: {
+    [key: string]: number;
+  },
+  tryNumber?: number;
+}
+
+interface Task {
+  id: string | null;
+  label: string | null;
+  instances: TaskInstance[];
+  tooltip?: string;
+  children?: Task[];
+  extraLinks?: string[];
+  isMapped?: boolean;
+}
+
+export type {
+  DagRun,
+  RunState,
+  TaskState,
+  TaskInstance,
+  Task,
+};
diff --git a/airflow/www/static/js/grid/utils/useSelection.test.jsx b/airflow/www/static/js/grid/utils/useSelection.test.tsx
similarity index 94%
rename from airflow/www/static/js/grid/utils/useSelection.test.jsx
rename to airflow/www/static/js/grid/utils/useSelection.test.tsx
index 2d2eeeb4db..19871c3ff0 100644
--- a/airflow/www/static/js/grid/utils/useSelection.test.jsx
+++ b/airflow/www/static/js/grid/utils/useSelection.test.tsx
@@ -25,7 +25,11 @@ import { MemoryRouter } from 'react-router-dom';
 
 import useSelection from './useSelection';
 
-const Wrapper = ({ children }) => (
+interface Props {
+  children: React.ReactNode;
+}
+
+const Wrapper = ({ children }: Props) => (
   <MemoryRouter>
     {children}
   </MemoryRouter>
@@ -47,7 +51,7 @@ describe('Test useSelection hook', () => {
 
   test.each([
     { taskId: 'task_1', runId: 'run_1' },
-    { taskId: null, runId: 'run_1' },
+    { runId: 'run_1', taskId: null },
     { taskId: 'task_1', runId: null },
   ])('Test onSelect() and clearSelection()', async (selected) => {
     const { result } = renderHook(() => useSelection(), { wrapper: Wrapper });
diff --git a/airflow/www/static/js/grid/utils/useSelection.js b/airflow/www/static/js/grid/utils/useSelection.ts
similarity index 91%
rename from airflow/www/static/js/grid/utils/useSelection.js
rename to airflow/www/static/js/grid/utils/useSelection.ts
index c90578837a..c4f5290b11 100644
--- a/airflow/www/static/js/grid/utils/useSelection.js
+++ b/airflow/www/static/js/grid/utils/useSelection.ts
@@ -22,6 +22,11 @@ import { useSearchParams } from 'react-router-dom';
 const RUN_ID = 'dag_run_id';
 const TASK_ID = 'task_id';
 
+export interface SelectionProps {
+  runId?: string | null ;
+  taskId?: string | null;
+}
+
 const useSelection = () => {
   const [searchParams, setSearchParams] = useSearchParams();
 
@@ -32,7 +37,7 @@ const useSelection = () => {
     setSearchParams(searchParams);
   };
 
-  const onSelect = ({ runId, taskId }) => {
+  const onSelect = ({ runId, taskId }: SelectionProps) => {
     const params = new URLSearchParams(searchParams);
 
     if (runId) params.set(RUN_ID, runId);
diff --git a/airflow/www/tsconfig.json b/airflow/www/tsconfig.json
index 5717d624e3..f264a9e3cc 100644
--- a/airflow/www/tsconfig.json
+++ b/airflow/www/tsconfig.json
@@ -25,8 +25,8 @@
     "strict": true,
     "allowJs": true,
     "importsNotUsedAsValues": "error",
-    "target": "esnext",
-    "module": "esnext",
+    "target": "ES6",
+    "module": "ES6",
     "moduleResolution": "node",
     "isolatedModules": true,
     "esModuleInterop": true,
diff --git a/airflow/www/webpack.config.js b/airflow/www/webpack.config.js
index e48b01c687..b4019c3238 100644
--- a/airflow/www/webpack.config.js
+++ b/airflow/www/webpack.config.js
@@ -106,7 +106,7 @@ const config = {
         ],
       },
       {
-        test: /\.[j|t]sx?$/,
+        test: /\.(js|jsx|tsx|ts)$/,
         exclude: /node_modules/,
         use: [
           {