This is an automated email from the ASF dual-hosted git repository. bbovenzi pushed a commit to branch mapped-task-drawer in repository https://gitbox.apache.org/repos/asf/airflow.git
commit e9ab6119a09fcf5a7b0907376cc157dfc8c80cdd Author: Brent Bovenzi <[email protected]> AuthorDate: Tue Mar 1 19:44:16 2022 -0500 use API --- airflow/www/package.json | 1 + airflow/www/static/js/tree/StatusBox.jsx | 6 +- airflow/www/static/js/tree/Tree.jsx | 16 ++-- .../tree/{details/content/Dag.jsx => api/index.js} | 21 +++-- .../{details/content/Dag.jsx => api/useDag.js} | 19 ++-- .../{details/content/Dag.jsx => api/useTasks.js} | 19 ++-- airflow/www/static/js/tree/dagRuns/Bar.jsx | 2 +- airflow/www/static/js/tree/details/Header.jsx | 5 +- airflow/www/static/js/tree/details/content/Dag.jsx | 36 ++++++-- .../js/tree/details/content/TaskInstance.jsx | 100 +++++++++++---------- airflow/www/static/js/tree/details/index.jsx | 34 ++++--- airflow/www/static/js/tree/index.jsx | 8 ++ airflow/www/yarn.lock | 12 +++ 13 files changed, 168 insertions(+), 111 deletions(-) diff --git a/airflow/www/package.json b/airflow/www/package.json index c5a4f3a..717d5b1 100644 --- a/airflow/www/package.json +++ b/airflow/www/package.json @@ -74,6 +74,7 @@ "@emotion/cache": "^11.4.0", "@emotion/react": "^11.4.1", "@emotion/styled": "^11", + "axios": "^0.26.0", "bootstrap-3-typeahead": "^4.0.2", "camelcase-keys": "^7.0.0", "codemirror": "^5.59.1", diff --git a/airflow/www/static/js/tree/StatusBox.jsx b/airflow/www/static/js/tree/StatusBox.jsx index 9609d9a..62762c5 100644 --- a/airflow/www/static/js/tree/StatusBox.jsx +++ b/airflow/www/static/js/tree/StatusBox.jsx @@ -32,7 +32,7 @@ import InstanceTooltip from './InstanceTooltip'; const StatusBox = ({ group, instance, containerRef, onSelect, selected, ...rest }) => { - const { runId } = instance; + const { runId, taskId } = instance; const { colors } = useTheme(); const hoverBlue = `${colors.blue[100]}50`; @@ -51,7 +51,9 @@ const StatusBox = ({ const onClick = (e) => { e.stopPropagation(); onMouseLeave(); - onSelect(instance); + onSelect({ + taskId, runId, instance, task: group, + }); }; return ( diff --git a/airflow/www/static/js/tree/Tree.jsx b/airflow/www/static/js/tree/Tree.jsx index 1f60d03..aeeaeb5 100644 --- a/airflow/www/static/js/tree/Tree.jsx +++ b/airflow/www/static/js/tree/Tree.jsx @@ -63,14 +63,14 @@ const Tree = () => { <Flex pl="24px" position="relative" flexDirection="row" justifyContent="space-between" ref={containerRef}> <Text transform="rotate(-90deg)" position="absolute" left="-6px" top="130px">Runs</Text> <Text transform="rotate(-90deg)" position="absolute" left="-6px" top="190px">Tasks</Text> - <Box mr="12px" pb="12px" overflowX="auto" ref={scrollRef} maxWidth="300px" minWidth="300px" position="relative"> - <FormControl display="flex" alignItems="center" justifyContent="flex-end" width="100%" mb={2}> - {isRefreshOn && <Spinner color="blue.500" speed="1s" mr="4px" />} - <FormLabel htmlFor="auto-refresh" mb={0} fontSize="12px" fontWeight="normal"> - Auto-refresh - </FormLabel> - <Switch id="auto-refresh" onChange={onToggleRefresh} isChecked={isRefreshOn} size="lg" /> - </FormControl> + <FormControl display="flex" position="absolute" left="220px"> + {isRefreshOn && <Spinner color="blue.500" speed="1s" mr="4px" />} + <FormLabel htmlFor="auto-refresh" mb={0} fontSize="12px" fontWeight="normal"> + Auto-refresh + </FormLabel> + <Switch id="auto-refresh" onChange={onToggleRefresh} isChecked={isRefreshOn} size="lg" /> + </FormControl> + <Box mr="12px" pb="12px" overflowX="auto" ref={scrollRef} maxWidth="300px" minWidth="300px" position="relative" mt="24px"> <Table height={0}> <Thead> <DagRuns diff --git a/airflow/www/static/js/tree/details/content/Dag.jsx b/airflow/www/static/js/tree/api/index.js similarity index 72% copy from airflow/www/static/js/tree/details/content/Dag.jsx copy to airflow/www/static/js/tree/api/index.js index be460ba..58edd88 100644 --- a/airflow/www/static/js/tree/details/content/Dag.jsx +++ b/airflow/www/static/js/tree/api/index.js @@ -17,15 +17,18 @@ * under the License. */ -import React from 'react'; -import { - Text, -} from '@chakra-ui/react'; +import axios from 'axios'; +import camelcaseKeys from 'camelcase-keys'; -const Dag = () => ( - <> - <Text>dag details</Text> - </> +import useDag from './useDag'; +import useTasks from './useTasks'; + +axios.defaults.baseURL = '/api/v1'; +axios.interceptors.response.use( + (res) => (res.data ? camelcaseKeys(res.data, { deep: true }) : res), ); -export default Dag; +export { + useDag, + useTasks, +}; diff --git a/airflow/www/static/js/tree/details/content/Dag.jsx b/airflow/www/static/js/tree/api/useDag.js similarity index 80% copy from airflow/www/static/js/tree/details/content/Dag.jsx copy to airflow/www/static/js/tree/api/useDag.js index be460ba..6c19ee4 100644 --- a/airflow/www/static/js/tree/details/content/Dag.jsx +++ b/airflow/www/static/js/tree/api/useDag.js @@ -17,15 +17,12 @@ * under the License. */ -import React from 'react'; -import { - Text, -} from '@chakra-ui/react'; +import axios from 'axios'; +import { useQuery } from 'react-query'; -const Dag = () => ( - <> - <Text>dag details</Text> - </> -); - -export default Dag; +export default function useDag(dagId) { + return useQuery( + ['dag', dagId], + () => axios.get(`/dags/${dagId}`), + ); +} diff --git a/airflow/www/static/js/tree/details/content/Dag.jsx b/airflow/www/static/js/tree/api/useTasks.js similarity index 80% copy from airflow/www/static/js/tree/details/content/Dag.jsx copy to airflow/www/static/js/tree/api/useTasks.js index be460ba..3dd11e30 100644 --- a/airflow/www/static/js/tree/details/content/Dag.jsx +++ b/airflow/www/static/js/tree/api/useTasks.js @@ -17,15 +17,12 @@ * under the License. */ -import React from 'react'; -import { - Text, -} from '@chakra-ui/react'; +import axios from 'axios'; +import { useQuery } from 'react-query'; -const Dag = () => ( - <> - <Text>dag details</Text> - </> -); - -export default Dag; +export default function useTasks(dagId) { + return useQuery( + ['tasks', dagId], + () => axios.get(`/dags/${dagId}/tasks`), + ); +} diff --git a/airflow/www/static/js/tree/dagRuns/Bar.jsx b/airflow/www/static/js/tree/dagRuns/Bar.jsx index 1c8dd6b..94c84e7 100644 --- a/airflow/www/static/js/tree/dagRuns/Bar.jsx +++ b/airflow/www/static/js/tree/dagRuns/Bar.jsx @@ -59,7 +59,7 @@ const DagRunBar = ({ zIndex={1} onClick={(e) => { e.stopPropagation(); - onSelect(run); + onSelect({ runId: run.runId, dagRun: run }); }} position="relative" data-peer diff --git a/airflow/www/static/js/tree/details/Header.jsx b/airflow/www/static/js/tree/details/Header.jsx index b9d3086..c362606 100644 --- a/airflow/www/static/js/tree/details/Header.jsx +++ b/airflow/www/static/js/tree/details/Header.jsx @@ -27,9 +27,8 @@ import { } from '@chakra-ui/react'; import { MdPlayArrow } from 'react-icons/md'; -import useTreeData from '../useTreeData'; import { formatDateTime } from '../../datetime_utils'; -import getMetaValue from '../../meta_value'; +import { getMetaValue } from '../../utils'; const dagId = getMetaValue('dag_id'); @@ -43,8 +42,8 @@ const LabelValue = ({ label, value }) => ( const Header = ({ selected: { taskId, runId }, onSelect, + dagRuns, }) => { - const { data: { dagRuns = [] } } = useTreeData(); const dagRun = dagRuns.find((r) => r.runId === runId); // console.log(dagRun); let runLabel = dagRun ? formatDateTime(dagRun.dataIntervalEnd) : ''; diff --git a/airflow/www/static/js/tree/details/content/Dag.jsx b/airflow/www/static/js/tree/details/content/Dag.jsx index be460ba..e71d9c2 100644 --- a/airflow/www/static/js/tree/details/content/Dag.jsx +++ b/airflow/www/static/js/tree/details/content/Dag.jsx @@ -20,12 +20,38 @@ import React from 'react'; import { Text, + Tag, + Code, + Flex, + HStack, } from '@chakra-ui/react'; -const Dag = () => ( - <> - <Text>dag details</Text> - </> -); +import { getMetaValue } from '../../../utils'; +import { useDag } from '../../api'; + +const dagId = getMetaValue('dag_id'); + +const Dag = () => { + const { data: dag } = useDag(dagId); + if (!dag) return null; + const { + description, tags, fileloc, owners, + } = dag; + return ( + <> + {description && <Text>{description}</Text>} + <HStack>{tags.map((tag) => <Tag key={tag.name} size="lg">{tag.name}</Tag>)}</HStack> + <Text> + Relative File Location: + {' '} + <Code colorScheme="blackAlpha">{fileloc}</Code> + </Text> + <Flex> + <Text mr={2}>Owner:</Text> + {owners.map((o) => <Text key={o}>{o}</Text>)} + </Flex> + </> + ); +}; export default Dag; diff --git a/airflow/www/static/js/tree/details/content/TaskInstance.jsx b/airflow/www/static/js/tree/details/content/TaskInstance.jsx index bc60737..0c0a21e 100644 --- a/airflow/www/static/js/tree/details/content/TaskInstance.jsx +++ b/airflow/www/static/js/tree/details/content/TaskInstance.jsx @@ -24,68 +24,70 @@ import { Text, Box, } from '@chakra-ui/react'; +import { finalStatesMap } from '../../../utils'; import { formatDateTime, getDuration, formatDuration } from '../../../datetime_utils'; const TaskInstance = ({ instance: { - duration, operator, startDate, endDate, state, taskId, runId, // mappedStates, + duration, operator, startDate, endDate, state, taskId, runId, mappedStates, }, + task, }) => { - const isGroup = false; // !!group.children; + const isGroup = !!task.children; const groupSummary = []; - // const mapSummary = []; + const mapSummary = []; - // if (isGroup) { - // const numMap = finalStatesMap(); - // 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); - // } - // }); - // numMap.forEach((key, val) => { - // if (key > 0) { - // groupSummary.push( - // // eslint-disable-next-line react/no-array-index-key - // <Text key={val} ml="10px"> - // {val} - // {': '} - // {key} - // </Text>, - // ); - // } - // }); - // } + if (isGroup) { + const numMap = finalStatesMap(); + task.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); + } + }); + numMap.forEach((key, val) => { + if (key > 0) { + groupSummary.push( + // eslint-disable-next-line react/no-array-index-key + <Text key={val} ml="10px"> + {val} + {': '} + {key} + </Text>, + ); + } + }); + } - // if (group.isMapped && mappedStates) { - // const numMap = finalStatesMap(); - // mappedStates.forEach((s) => { - // const stateKey = s || 'no_status'; - // if (numMap.has(stateKey)) numMap.set(stateKey, numMap.get(stateKey) + 1); - // }); - // numMap.forEach((key, val) => { - // if (key > 0) { - // mapSummary.push( - // // eslint-disable-next-line react/no-array-index-key - // <Text key={val} ml="10px"> - // {val} - // {': '} - // {key} - // </Text>, - // ); - // } - // }); - // } + if (task.isMapped && mappedStates) { + const numMap = finalStatesMap(); + mappedStates.forEach((s) => { + const stateKey = s || 'no_status'; + if (numMap.has(stateKey)) numMap.set(stateKey, numMap.get(stateKey) + 1); + }); + numMap.forEach((key, val) => { + if (key > 0) { + mapSummary.push( + // eslint-disable-next-line react/no-array-index-key + <Text key={val} ml="10px"> + {val} + {': '} + {key} + </Text>, + ); + } + }); + } const taskIdTitle = isGroup ? 'Task Group Id: ' : 'Task Id: '; return ( <Box fontSize="12px" py="4px"> - {/* {group.tooltip && ( - <Text>{group.tooltip}</Text> - )} */} + {task.tooltip && ( + <Text>{task.tooltip}</Text> + )} <Text> <Text as="strong">Status:</Text> {' '} @@ -98,7 +100,7 @@ const TaskInstance = ({ {groupSummary} </> )} - {/* {group.isMapped && ( + {task.isMapped && ( <> <br /> <Text as="strong"> @@ -109,7 +111,7 @@ const TaskInstance = ({ </Text> {mapSummary} </> - )} */} + )} <br /> <Text> {taskIdTitle} diff --git a/airflow/www/static/js/tree/details/index.jsx b/airflow/www/static/js/tree/details/index.jsx index 31610a7..ee3b044 100644 --- a/airflow/www/static/js/tree/details/index.jsx +++ b/airflow/www/static/js/tree/details/index.jsx @@ -28,21 +28,31 @@ import Header from './Header'; import TaskInstanceContent from './content/TaskInstance'; import DagRunContent from './content/DagRun'; import DagContent from './content/Dag'; +import useTreeData from '../useTreeData'; const Details = ({ selected, onSelect, -}) => ( - <Flex borderLeftWidth="1px" flexDirection="column" p={3} flexGrow={1}> - <Header selected={selected} onSelect={onSelect} /> - <Divider my={2} /> - <Box> - {/* TODO: get full instance data from the API */} - {!selected.runId && !selected.taskId && <DagContent />} - {selected.runId && !selected.taskId && <DagRunContent dagRun={selected} />} - {selected.taskId && <TaskInstanceContent instance={selected} />} - </Box> - </Flex> -); +}) => { + const { data: { dagRuns = [] } } = useTreeData(); + console.log(selected); + return ( + <Flex borderLeftWidth="1px" flexDirection="column" p={3} flexGrow={1}> + <Header selected={selected} onSelect={onSelect} dagRuns={dagRuns} /> + <Divider my={2} /> + <Box> + {/* TODO: get full instance data from the API */} + {!selected.runId && !selected.taskId && <DagContent />} + {selected.runId && !selected.taskId && <DagRunContent dagRun={selected.dagRun} />} + {selected.taskId && ( + <TaskInstanceContent + instance={selected.instance} + task={selected.task} + /> + )} + </Box> + </Flex> + ); +}; export default Details; diff --git a/airflow/www/static/js/tree/index.jsx b/airflow/www/static/js/tree/index.jsx index 2c86478..a509d25 100644 --- a/airflow/www/static/js/tree/index.jsx +++ b/airflow/www/static/js/tree/index.jsx @@ -38,6 +38,14 @@ const mainElement = document.getElementById('react-container'); shadowRoot.appendChild(mainElement); const queryClient = new QueryClient(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + }, + }, +}); + function App() { return ( <React.StrictMode> diff --git a/airflow/www/yarn.lock b/airflow/www/yarn.lock index 26d00cb..7da2551 100644 --- a/airflow/www/yarn.lock +++ b/airflow/www/yarn.lock @@ -3254,6 +3254,13 @@ axe-core@^4.0.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.3.tgz#b55cd8e8ddf659fe89b064680e1c6a4dceab0325" integrity sha512-/lqqLAmuIPi79WYfRpy2i8z+x+vxU3zX2uAm0gs1q52qTuKwolOj1P8XbufpXcsydrpKx2yGn2wzAnxCMV86QA== +axios@^0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.0.tgz#9a318f1c69ec108f8cd5f3c3d390366635e13928" + integrity sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og== + dependencies: + follow-redirects "^1.14.8" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -6018,6 +6025,11 @@ focus-lock@^0.9.1: dependencies: tslib "^2.0.3" +follow-redirects@^1.14.8: + version "1.14.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" + integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
