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: [
{