From e4065e47e0a4e746fcfb52cb151243a3707e9502 Mon Sep 17 00:00:00 2001 From: Brent Bovenzi Date: Tue, 14 Jun 2022 15:44:00 -0400 Subject: [PATCH 01/10] convert all useSelection files to ts Update grid data ts, remove some anys --- airflow/www/package.json | 1 + .../www/static/js/grid/{Main.jsx => Main.tsx} | 9 ++- airflow/www/static/js/grid/ToggleGroups.jsx | 8 +- .../static/js/grid/api/{index.js => index.ts} | 4 +- .../api/{useGridData.js => useGridData.ts} | 79 +++++++++++-------- .../static/js/grid/context/autorefresh.jsx | 8 +- .../js/grid/dagRuns/{index.jsx => index.tsx} | 13 +-- .../static/js/grid/details/BreadcrumbText.tsx | 38 +++++++++ .../grid/details/{Header.jsx => Header.tsx} | 30 +++---- .../js/grid/details/{index.jsx => index.tsx} | 16 ++-- airflow/www/static/js/grid/index.d.ts | 26 ++++++ ...kRows.test.jsx => renderTaskRows.test.tsx} | 0 ...{renderTaskRows.jsx => renderTaskRows.tsx} | 49 ++++++++---- airflow/www/static/js/grid/types/index.ts | 69 ++++++++++++++++ ...lection.test.jsx => useSelection.test.tsx} | 10 ++- .../{useSelection.js => useSelection.ts} | 7 +- airflow/www/tsconfig.json | 4 +- airflow/www/webpack.config.js | 23 +++++- 18 files changed, 294 insertions(+), 100 deletions(-) rename airflow/www/static/js/grid/{Main.jsx => Main.tsx} (91%) rename airflow/www/static/js/grid/api/{index.js => index.ts} (93%) rename airflow/www/static/js/grid/api/{useGridData.js => useGridData.ts} (53%) rename airflow/www/static/js/grid/dagRuns/{index.jsx => index.tsx} (90%) create mode 100644 airflow/www/static/js/grid/details/BreadcrumbText.tsx rename airflow/www/static/js/grid/details/{Header.jsx => Header.tsx} (83%) rename airflow/www/static/js/grid/details/{index.jsx => index.tsx} (81%) create mode 100644 airflow/www/static/js/grid/index.d.ts rename airflow/www/static/js/grid/{renderTaskRows.test.jsx => renderTaskRows.test.tsx} (100%) rename airflow/www/static/js/grid/{renderTaskRows.jsx => renderTaskRows.tsx} (77%) create mode 100644 airflow/www/static/js/grid/types/index.ts rename airflow/www/static/js/grid/utils/{useSelection.test.jsx => useSelection.test.tsx} (93%) rename airflow/www/static/js/grid/utils/{useSelection.js => useSelection.ts} (92%) diff --git a/airflow/www/package.json b/airflow/www/package.json index ad11cc3a0d976..685a2cfbc7d57 100644 --- a/airflow/www/package.json +++ b/airflow/www/package.json @@ -67,6 +67,7 @@ "stylelint": "^13.6.1", "stylelint-config-standard": "^20.0.0", "terser-webpack-plugin": "<5.0.0", + "ts-loader": "^8.2.0", "typescript": "^4.6.3", "url-loader": "4.1.0", "webpack": "^5.73.0", diff --git a/airflow/www/static/js/grid/Main.jsx b/airflow/www/static/js/grid/Main.tsx similarity index 91% rename from airflow/www/static/js/grid/Main.jsx rename to airflow/www/static/js/grid/Main.tsx index 5e9a4f2a9a12d..4e7ac87c9ff05 100644 --- a/airflow/www/static/js/grid/Main.jsx +++ b/airflow/www/static/js/grid/Main.tsx @@ -38,8 +38,9 @@ import { useGridData } from './api'; const detailsPanelKey = 'hideDetailsPanel'; -const Main = () => { - const { data: { groups }, isLoading } = useGridData(); +const Main: React.FC = () => { + const { data, isLoading } = useGridData(); + const groups = (data as any)?.groups || {}; const isPanelOpen = localStorage.getItem(detailsPanelKey) !== 'true'; const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: isPanelOpen }); const { clearSelection } = useSelection(); @@ -47,10 +48,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 3705d67d80241..2f027cd668776 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 3487ecd6eaff0..0ac8e4e28410d 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 53% rename from airflow/www/static/js/grid/api/useGridData.js rename to airflow/www/static/js/grid/api/useGridData.ts index 38d4e00748d32..44bbc8e4c41a9 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 { GridTask, 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: GridTask; +} + +const emptyData: 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,34 +60,37 @@ const useGridData = () => { }, } = useFilters(); - return useQuery(['gridData', baseDate, numRuns, runType, runState], async () => { - try { - const params = { - root: urlRoot || undefined, - [DAG_ID_PARAM]: dagId, - [BASE_DATE_PARAM]: baseDate === now ? undefined : baseDate, - [NUM_RUNS_PARAM]: numRuns, - [RUN_TYPE_PARAM]: runType, - [RUN_STATE_PARAM]: runState, - }; - const newData = await axios.get(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, - }); + return useQuery( + ['gridData', baseDate, numRuns, runType, runState], + async () => { + try { + const params = { + root: urlRoot || undefined, + [DAG_ID_PARAM]: dagId, + [BASE_DATE_PARAM]: baseDate === now ? undefined : baseDate, + [NUM_RUNS_PARAM]: numRuns, + [RUN_TYPE_PARAM]: runType, + [RUN_STATE_PARAM]: runState, + }; + const response = await axios.get(gridDataUrl, { params }); + // turn off auto refresh if there are no active runs + if (!areActiveRuns(response.dagRuns)) stopRefresh(); + return response; + } catch (error) { + stopRefresh(); + errorToast({ + title: 'Auto-refresh Error', + error, + }); + throw (error); + } + }, { + placeholderData: emptyData, + // only refetch if the refresh switch is on + refetchInterval: isRefreshOn && (autoRefreshInterval || 1) * 1000, + keepPreviousData: true, + }, + ); }; export default useGridData; diff --git a/airflow/www/static/js/grid/context/autorefresh.jsx b/airflow/www/static/js/grid/context/autorefresh.jsx index 35df9b7daf920..11c987fe34493 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/dagRuns/index.jsx b/airflow/www/static/js/grid/dagRuns/index.tsx similarity index 90% rename from airflow/www/static/js/grid/dagRuns/index.jsx rename to airflow/www/static/js/grid/dagRuns/index.tsx index ec588313c6150..90f79798911dd 100644 --- a/airflow/www/static/js/grid/dagRuns/index.jsx +++ b/airflow/www/static/js/grid/dagRuns/index.tsx @@ -24,23 +24,26 @@ 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: React.FC = ({ children, ...rest }) => ( {children} ); -const DagRuns = () => { - const { data: { dagRuns } } = useGridData(); +const DagRuns: React.FC = () => { + const { data } = useGridData(); + const dagRuns: DagRun[] = (data as any)?.dagRuns || []; 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 +94,7 @@ const DagRuns = () => { - {runs.map((run, i) => ( + {runs.map((run: DagRun, i: number) => ( = ({ label, value }) => ( + + {label} + {value} + +); + +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 83% rename from airflow/www/static/js/grid/details/Header.jsx rename to airflow/www/static/js/grid/details/Header.tsx index c158deabcaa59..85ac41e16004d 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,18 @@ import { getMetaValue } from '../../utils'; import useSelection from '../utils/useSelection'; import Time from '../components/Time'; import { useTasks, useGridData } from '../api'; +import BreadcrumbText from './BreadcrumbText'; +import type { DagRun } from '../types'; const dagId = getMetaValue('dag_id'); -const LabelValue = ({ label, value }) => ( - - {label} - {value} - -); +const Header: React.FC = () => { + const { data: gridData } = useGridData(); + const { data: taskData } = useTasks(); + const dagRuns: DagRun[] = (gridData as any)?.dagRuns || []; + const tasks: any[] = (taskData as any)?.tasks || []; -const Header = () => { - const { data: { dagRuns } } = useGridData(); const { selected: { taskId, runId }, onSelect, clearSelection } = useSelection(); - const { data: { tasks } } = useTasks(); const dagRun = dagRuns.find((r) => r.runId === runId); const task = tasks.find((t) => t.taskId === taskId); @@ -59,7 +55,7 @@ const Header = () => { }, [clearSelection, dagRun, runId]); let runLabel; - if (dagRun) { + if (dagRun && runId) { if (runId.includes('manual__') || runId.includes('scheduled__') || runId.includes('backfill__')) { runLabel = (