This is an automated email from the ASF dual-hosted git repository. bbovenzi pushed a commit to branch grid-list-mapped-tis in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 5c965d8be4d0a9ba598053d0ffd83fa66b61663c Author: Brent Bovenzi <[email protected]> AuthorDate: Fri Apr 1 10:29:45 2022 -0400 Add table of mapped instances to grid view --- airflow/www/package.json | 1 + airflow/www/static/js/tree/Table.jsx | 168 +++++++++++++++++++++ airflow/www/static/js/tree/api/index.js | 2 + .../www/static/js/tree/api/useMappedInstances.js | 33 ++++ .../js/tree/details/content/MappedInstances.jsx | 136 +++++++++++++++++ .../js/tree/details/content/taskInstance/index.jsx | 4 + airflow/www/yarn.lock | 5 + 7 files changed, 349 insertions(+) diff --git a/airflow/www/package.json b/airflow/www/package.json index 9ee33c94..d15cd05 100644 --- a/airflow/www/package.json +++ b/airflow/www/package.json @@ -95,6 +95,7 @@ "react-dom": "^17.0.2", "react-icons": "^4.3.1", "react-query": "^3.34.16", + "react-table": "^7.7.0", "redoc": "^2.0.0-rc.63", "url-search-params-polyfill": "^8.1.0" }, diff --git a/airflow/www/static/js/tree/Table.jsx b/airflow/www/static/js/tree/Table.jsx new file mode 100644 index 0000000..2322f5a --- /dev/null +++ b/airflow/www/static/js/tree/Table.jsx @@ -0,0 +1,168 @@ +/*! + * 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. + */ + +/* + * Custom wrapper of react-table using Chakra UI components +*/ + +import React, { useEffect } from 'react'; +import { + Flex, + Table as ChakraTable, + Thead, + Tbody, + Tr, + Th, + Td, + IconButton, + Text, + useColorModeValue, +} from '@chakra-ui/react'; +import { + useTable, useSortBy, usePagination, +} from 'react-table'; +import { + MdKeyboardArrowLeft, MdKeyboardArrowRight, +} from 'react-icons/md'; +import { + TiArrowUnsorted, TiArrowSortedDown, TiArrowSortedUp, +} from 'react-icons/ti'; + +const Table = ({ + data, columns, manualPagination, pageSize = 25, setSortBy, +}) => { + const { totalEntries, offset, setOffset } = manualPagination || {}; + const oddColor = useColorModeValue('gray.50', 'gray.900'); + const hoverColor = useColorModeValue('gray.100', 'gray.700'); + + const pageCount = totalEntries ? (Math.ceil(totalEntries / pageSize) || 1) : data.length; + + const lowerCount = (offset || 0) + 1; + const upperCount = lowerCount + data.length - 1; + + const { + getTableProps, + getTableBodyProps, + allColumns, + prepareRow, + page, + canPreviousPage, + canNextPage, + nextPage, + previousPage, + state: { pageIndex, sortBy }, + } = useTable( + { + columns, + data, + pageCount, + manualPagination: !!manualPagination, + manualSortBy: !!setSortBy, + initialState: { + pageIndex: offset ? offset / pageSize : 0, + pageSize, + }, + }, + useSortBy, + usePagination, + ); + + const handleNext = () => { + nextPage(); + if (setOffset) setOffset((pageIndex + 1) * pageSize); + }; + + const handlePrevious = () => { + previousPage(); + if (setOffset) setOffset((pageIndex - 1 || 0) * pageSize); + }; + + useEffect(() => { + if (setSortBy) setSortBy(sortBy); + }, [sortBy, setSortBy]); + + return ( + <> + <ChakraTable {...getTableProps()}> + <Thead> + <Tr> + {allColumns.map((column) => ( + <Th + {...column.getHeaderProps(column.getSortByToggleProps())} + > + {column.render('Header')} + {column.isSorted && ( + column.isSortedDesc ? ( + <TiArrowSortedDown aria-label="sorted descending" style={{ display: 'inline' }} size="1em" /> + ) : ( + <TiArrowSortedUp aria-label="sorted ascending" style={{ display: 'inline' }} size="1em" /> + ) + )} + {(!column.isSorted && column.canSort) && (<TiArrowUnsorted aria-label="unsorted" style={{ display: 'inline' }} size="1em" />)} + </Th> + ))} + </Tr> + </Thead> + <Tbody {...getTableBodyProps()}> + {!data.length && ( + <Tr> + <Td colSpan={2}>No Data found.</Td> + </Tr> + )} + {page.map((row) => { + prepareRow(row); + return ( + <Tr + {...row.getRowProps()} + _odd={{ backgroundColor: oddColor }} + _hover={{ backgroundColor: hoverColor }} + > + {row.cells.map((cell) => ( + <Td + {...cell.getCellProps()} + py={3} + > + {cell.render('Cell')} + </Td> + ))} + </Tr> + ); + })} + </Tbody> + </ChakraTable> + <Flex alignItems="center" justifyContent="flex-start" my={4}> + <IconButton variant="ghost" onClick={handlePrevious} disabled={!canPreviousPage} aria-label="Previous Page"> + <MdKeyboardArrowLeft /> + </IconButton> + <IconButton variant="ghost" onClick={handleNext} disabled={!canNextPage} aria-label="Next Page"> + <MdKeyboardArrowRight /> + </IconButton> + <Text> + {lowerCount} + - + {upperCount} + {' of '} + {totalEntries} + </Text> + </Flex> + </> + ); +}; + +export default Table; diff --git a/airflow/www/static/js/tree/api/index.js b/airflow/www/static/js/tree/api/index.js index 1c169c9..aea1e91 100644 --- a/airflow/www/static/js/tree/api/index.js +++ b/airflow/www/static/js/tree/api/index.js @@ -33,6 +33,7 @@ import useMarkSuccessTask from './useMarkSuccessTask'; import useExtraLinks from './useExtraLinks'; import useConfirmMarkTask from './useConfirmMarkTask'; import useTreeData from './useTreeData'; +import useMappedInstances from './useMappedInstances'; axios.interceptors.response.use( (res) => (res.data ? camelcaseKeys(res.data, { deep: true }) : res), @@ -54,4 +55,5 @@ export { useExtraLinks, useConfirmMarkTask, useTreeData, + useMappedInstances, }; diff --git a/airflow/www/static/js/tree/api/useMappedInstances.js b/airflow/www/static/js/tree/api/useMappedInstances.js new file mode 100644 index 0000000..844a194 --- /dev/null +++ b/airflow/www/static/js/tree/api/useMappedInstances.js @@ -0,0 +1,33 @@ +/*! + * 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. + */ + +import axios from 'axios'; +import { useQuery } from 'react-query'; + +export default function useMappedInstances({ + dagId, runId, taskId, limit, offset, order, +}) { + const orderParam = order && order !== 'map_index' ? { order_by: order } : {}; + return useQuery( + ['mappedInstances', dagId, runId, taskId, offset, order], + () => axios.get(`/api/v1/dags/${dagId}/dagRuns/${runId}/taskInstances/${taskId}/listMapped`, { + params: { offset, limit, ...orderParam }, + }), + ); +} diff --git a/airflow/www/static/js/tree/details/content/MappedInstances.jsx b/airflow/www/static/js/tree/details/content/MappedInstances.jsx new file mode 100644 index 0000000..f78e533 --- /dev/null +++ b/airflow/www/static/js/tree/details/content/MappedInstances.jsx @@ -0,0 +1,136 @@ +/*! + * 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. + */ + +import React, { useState, useMemo } from 'react'; +import { + Flex, + Text, + Box, + Link, + Button, +} from '@chakra-ui/react'; +import { snakeCase } from 'lodash'; + +import { formatDateTime, formatDuration } from '../../../datetime_utils'; +import { useMappedInstances } from '../../api'; +import { SimpleStatus } from '../../StatusBox'; +import Table from '../../Table'; + +const MappedInstances = ({ + dagId, runId, taskId, +}) => { + const limit = 25; + const [offset, setOffset] = useState(0); + const [sortBy, setSortBy] = useState([]); + + const sort = sortBy[0]; + + const order = sort && (sort.id === 'state' || sort.id === 'mapIndex') ? `${sort.desc ? '-' : ''}${snakeCase(sort.id)}` : ''; + + const { + data: { taskInstances, totalEntries } = { taskInstances: [], totalEntries: 0 }, + } = useMappedInstances({ + dagId, runId, taskId, limit, offset, order, + }); + + const data = useMemo( + () => taskInstances.map((mi) => { + const params = new URLSearchParams({ + dag_id: dagId, + task_id: mi.taskId, + execution_date: mi.executionDate, + map_index: mi.mapIndex, + }).toString(); + const logLink = `/log?${params}`; + const detailsLink = `/task?${params}`; + return { + ...mi, + state: ( + <Flex alignItems="center"> + <SimpleStatus state={mi.state} mx={2} /> + {mi.state || 'no status'} + </Flex> + ), + duration: formatDuration(mi.duration), + startDate: formatDateTime(mi.startDate), + endDate: formatDateTime(mi.endDate), + links: ( + <Flex alignItems="center"> + <Button as={Link} variant="outline" mr={1} colorScheme="blue" href={logLink}>Log</Button> + <Button as={Link} variant="outline" colorScheme="blue" href={detailsLink}>More Details</Button> + </Flex> + ), + }; + }), + [dagId, taskInstances], + ); + + const columns = useMemo( + () => [ + { + Header: 'Map Index', + accessor: 'mapIndex', + }, + { + Header: 'State', + accessor: 'state', + }, + { + Header: 'Duration', + accessor: 'duration', + disableSortBy: true, + }, + { + Header: 'Start Date', + accessor: 'startDate', + disableSortBy: true, + }, + { + Header: 'End Date', + accessor: 'endDate', + disableSortBy: true, + }, + { + disableSortBy: true, + accessor: 'links', + }, + ], + [], + ); + + return ( + <Box> + <br /> + <Text as="strong">Mapped Instances</Text> + <Table + data={data} + columns={columns} + manualPagination={{ + offset, + setOffset, + totalEntries, + }} + pageSize={limit} + setSortBy={setSortBy} + /> + </Box> + ); +}; + +export default MappedInstances; diff --git a/airflow/www/static/js/tree/details/content/taskInstance/index.jsx b/airflow/www/static/js/tree/details/content/taskInstance/index.jsx index 8457d99..ed970f4 100644 --- a/airflow/www/static/js/tree/details/content/taskInstance/index.jsx +++ b/airflow/www/static/js/tree/details/content/taskInstance/index.jsx @@ -35,6 +35,7 @@ import TaskNav from './Nav'; import Details from './Details'; import { useTreeData } from '../../../api'; +import MappedInstances from '../MappedInstances'; const getTask = ({ taskId, runId, task }) => { if (task.id === taskId) return task; @@ -100,6 +101,9 @@ const TaskInstance = ({ taskId, runId }) => { executionDate={executionDate} extraLinks={task.extraLinks} /> + {task.isMapped && ( + <MappedInstances dagId={dagId} runId={runId} taskId={taskId} /> + )} </Box> ); }; diff --git a/airflow/www/yarn.lock b/airflow/www/yarn.lock index 97cfb66..9804dc1 100644 --- a/airflow/www/yarn.lock +++ b/airflow/www/yarn.lock @@ -9838,6 +9838,11 @@ react-style-singleton@^2.1.0: invariant "^2.2.4" tslib "^1.0.0" +react-table@^7.7.0: + version "7.7.0" + resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.7.0.tgz#e2ce14d7fe3a559f7444e9ecfe8231ea8373f912" + integrity sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA== + react-tabs@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-3.2.2.tgz#07bdc3cdb17bdffedd02627f32a93cd4b3d6e4d0"
