Skip to content

Commit

Permalink
Updated table component (apache#15805)
Browse files Browse the repository at this point in the history
* use react-table for table data

* add skeleton loader to table

* refactor loading data

* UI pagination

* server-side pagination

* componentize custom react table

* fix placeholder switch

* add test that errors are rendered

* Update airflow/ui/src/interfaces/react-table-config.d.ts

remove extraneous comment

Co-authored-by: Ash Berlin-Taylor <[email protected]>

* Update airflow/ui/src/components/Table.tsx

Co-authored-by: Ryan Hamilton <[email protected]>

* update sort icons and pagination display

Co-authored-by: Ash Berlin-Taylor <[email protected]>
Co-authored-by: Ryan Hamilton <[email protected]>
  • Loading branch information
3 people authored May 27, 2021
1 parent 65519ab commit 2c6b003
Show file tree
Hide file tree
Showing 8 changed files with 531 additions and 125 deletions.
2 changes: 2 additions & 0 deletions airflow/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"react-query": "^3.12.3",
"react-router-dom": "^5.2.0",
"react-select": "^4.3.0",
"react-table": "^7.7.0",
"use-react-router": "^1.0.7"
},
"devDependencies": {
Expand All @@ -40,6 +41,7 @@
"@types/react-dom": "^17.0.2",
"@types/react-router-dom": "^5.1.7",
"@types/react-select": "^4.0.15",
"@types/react-table": "^7.7.0",
"eslint": "^7",
"eslint-config-airbnb-typescript": "^12.3.1",
"eslint-plugin-import": "^2.22.1",
Expand Down
23 changes: 15 additions & 8 deletions airflow/ui/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,17 @@ setLogger({
const toastDuration = 3000;
const refetchInterval = isTest ? false : 1000;

export function useDags() {
interface PageProps {
offset?: number;
limit?: number
}

export function useDags({ offset = 0, limit }: PageProps) {
return useQuery<DagsResponse, Error>(
'dags',
(): Promise<DagsResponse> => axios.get('/dags'),
['dags', offset],
(): Promise<DagsResponse> => axios.get('/dags', {
params: { offset, limit },
}),
{
refetchInterval,
retry: !isTest,
Expand Down Expand Up @@ -132,7 +139,7 @@ export function useTriggerRun(dagId: Dag['dagId']) {
);
}

export function useSaveDag(dagId: Dag['dagId']) {
export function useSaveDag(dagId: Dag['dagId'], offset: number) {
const queryClient = useQueryClient();
const toast = useToast();
return useMutation(
Expand All @@ -141,7 +148,7 @@ export function useSaveDag(dagId: Dag['dagId']) {
onMutate: async (updatedValues: Record<string, any>) => {
await queryClient.cancelQueries(['dag', dagId]);
const previousDag = queryClient.getQueryData(['dag', dagId]) as Dag;
const previousDags = queryClient.getQueryData('dags') as DagsResponse;
const previousDags = queryClient.getQueryData(['dags', offset]) as DagsResponse;

const newDags = previousDags.dags.map((dag) => (
dag.dagId === dagId ? { ...dag, ...updatedValues } : dag
Expand All @@ -153,7 +160,7 @@ export function useSaveDag(dagId: Dag['dagId']) {

// optimistically set the dag before the async request
queryClient.setQueryData(['dag', dagId], () => newDag);
queryClient.setQueryData('dags', (old) => ({
queryClient.setQueryData(['dags', offset], (old) => ({
...(old as Dag[]),
...{
dags: newDags,
Expand All @@ -168,7 +175,7 @@ export function useSaveDag(dagId: Dag['dagId']) {
// rollback to previous cache on error
if (error) {
queryClient.setQueryData(['dag', dagId], previousDag);
queryClient.setQueryData('dags', previousDags);
queryClient.setQueryData(['dags', offset], previousDags);
toast({
title: 'Error updating pipeline',
description: (error as Error).message,
Expand All @@ -180,7 +187,7 @@ export function useSaveDag(dagId: Dag['dagId']) {
// check if server response is different from our optimistic update
if (JSON.stringify(res) !== JSON.stringify(previousDag)) {
queryClient.setQueryData(['dag', dagId], res);
queryClient.setQueryData('dags', {
queryClient.setQueryData(['dags', offset], {
dags: previousDags.dags.map((dag) => (
dag.dagId === dagId ? res : dag
)),
Expand Down
187 changes: 187 additions & 0 deletions airflow/ui/src/components/Table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*!
* 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, Column, usePagination, SortingRule,
} from 'react-table';
import {
MdKeyboardArrowLeft, MdKeyboardArrowRight,
} from 'react-icons/md';
import {
TiArrowUnsorted, TiArrowSortedDown, TiArrowSortedUp,
} from 'react-icons/ti';

interface Props {
data: any[];
columns: Column<any>[];
/*
* manualPagination is when you need to do server-side pagination.
* Leave blank for client-side only
*/
manualPagination?: {
offset: number;
setOffset: (off: number) => void;
totalEntries: number;
};
/*
* setSortBy is for custom sorting such as server-side sorting
*/
setSortBy?: (sortBy: SortingRule<object>[]) => void;
pageSize?: number;
}

const Table: React.FC<Props> = ({
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;
Loading

0 comments on commit 2c6b003

Please sign in to comment.