Skip to content

Commit

Permalink
feat: features list pagination (#5496)
Browse files Browse the repository at this point in the history
New paginated table - tested on /features-new behind a flag
  • Loading branch information
Tymek committed Dec 1, 2023
1 parent be17b7f commit 755c22f
Show file tree
Hide file tree
Showing 28 changed files with 543 additions and 564 deletions.
1 change: 0 additions & 1 deletion frontend/orval.config.js
Expand Up @@ -15,7 +15,6 @@ module.exports = {
target: 'apis',
schemas: 'models',
client: 'swr',
prettier: true,
clean: true,
// mock: true,
override: {
Expand Down
4 changes: 4 additions & 0 deletions frontend/package.json
Expand Up @@ -38,6 +38,7 @@
"@mui/icons-material": "5.11.9",
"@mui/lab": "5.0.0-alpha.120",
"@mui/material": "5.11.10",
"@tanstack/react-table": "^8.10.7",
"@testing-library/dom": "8.20.1",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "12.1.5",
Expand All @@ -47,6 +48,7 @@
"@types/deep-diff": "1.0.5",
"@types/jest": "29.5.10",
"@types/lodash.clonedeep": "4.5.9",
"@types/lodash.mapvalues": "^4.6.9",
"@types/lodash.omit": "4.5.9",
"@types/node": "18.17.19",
"@types/react": "17.0.71",
Expand Down Expand Up @@ -79,6 +81,7 @@
"immer": "9.0.21",
"jsdom": "22.1.0",
"lodash.clonedeep": "4.5.0",
"lodash.mapvalues": "^4.6.0",
"lodash.omit": "4.5.0",
"mermaid": "^9.3.0",
"millify": "^6.0.0",
Expand All @@ -105,6 +108,7 @@
"swr": "2.2.4",
"tss-react": "4.9.3",
"typescript": "4.8.4",
"use-query-params": "^2.2.1",
"vanilla-jsoneditor": "^0.19.0",
"vite": "4.5.0",
"vite-plugin-env-compatible": "1.1.1",
Expand Down
Expand Up @@ -15,7 +15,6 @@ const StyledChip = styled(
)(({ theme, isActive = false }) => ({
borderRadius: `${theme.shape.borderRadius}px`,
padding: 0,
margin: theme.spacing(0, 0, 1, 0),
fontSize: theme.typography.body2.fontSize,
...(isActive
? {
Expand Down
Expand Up @@ -33,6 +33,7 @@ export const FavoriteIconHeader: VFC<IFavoriteIconHeaderProps> = ({
<IconButton
sx={{
mx: -0.75,
my: -1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
Expand Down
114 changes: 114 additions & 0 deletions frontend/src/component/common/Table/PaginatedTable/PaginatedTable.tsx
@@ -0,0 +1,114 @@
import { TableBody, TableRow, TableHead } from '@mui/material';
import { Table } from 'component/common/Table/Table/Table';
import {
Header,
type Table as TableType,
flexRender,
} from '@tanstack/react-table';
import { TableCell } from '../TableCell/TableCell';
import { CellSortable } from '../SortableTableHeader/CellSortable/CellSortable';
import { StickyPaginationBar } from 'component/common/Table/StickyPaginationBar/StickyPaginationBar';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';

const HeaderCell = <T extends object>(header: Header<T, unknown>) => {
const column = header.column;
const isDesc = column.getIsSorted() === 'desc';
const align = column.columnDef.meta?.align || undefined;

return (
<CellSortable
isSortable={column.getCanSort()}
isSorted={column.getIsSorted() !== false}
isDescending={isDesc}
align={align}
onClick={() => column.toggleSorting()}
styles={{ borderRadius: '0px' }}
>
{header.isPlaceholder
? null
: flexRender(column.columnDef.header, header.getContext())}
</CellSortable>
);
};

/**
* Use with react-table v8
*/
export const PaginatedTable = <T extends object>({
totalItems,
tableInstance,
}: {
tableInstance: TableType<T>;
totalItems?: number;
}) => {
const { pagination } = tableInstance.getState();

return (
<>
<Table>
<TableHead>
{tableInstance.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<HeaderCell {...header} key={header.id} />
))}
</TableRow>
))}
</TableHead>
<TableBody
role='rowgroup'
sx={{
'& tr': {
'&:hover': {
'.show-row-hover': {
opacity: 1,
},
},
},
}}
>
{tableInstance.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
<ConditionallyRender
condition={tableInstance.getRowModel().rows.length > 0}
show={
<StickyPaginationBar
totalItems={totalItems}
pageIndex={pagination.pageIndex}
pageSize={pagination.pageSize}
fetchNextPage={() =>
tableInstance.setPagination({
pageIndex: pagination.pageIndex + 1,
pageSize: pagination.pageSize,
})
}
fetchPrevPage={() =>
tableInstance.setPagination({
pageIndex: pagination.pageIndex - 1,
pageSize: pagination.pageSize,
})
}
setPageLimit={(pageSize) =>
tableInstance.setPagination({
pageIndex: 0,
pageSize,
})
}
/>
}
/>
</>
);
};
@@ -1,6 +1,6 @@
import React from 'react';
import { Box, Typography, Button, styled } from '@mui/material';
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from '../../ConditionallyRender/ConditionallyRender';
import { ReactComponent as ArrowRight } from 'assets/icons/arrowRight.svg';
import { ReactComponent as ArrowLeft } from 'assets/icons/arrowLeft.svg';

Expand Down Expand Up @@ -44,51 +44,42 @@ const StyledSelect = styled('select')(({ theme }) => ({
}));

interface PaginationBarProps {
total: number;
currentOffset: number;
totalItems?: number;
pageIndex: number;
pageSize: number;
fetchPrevPage: () => void;
fetchNextPage: () => void;
hasPreviousPage: boolean;
hasNextPage: boolean;
pageLimit: number;
setPageLimit: (limit: number) => void;
}

export const PaginationBar: React.FC<PaginationBarProps> = ({
total,
currentOffset,
totalItems,
pageSize,
pageIndex = 0,
fetchPrevPage,
fetchNextPage,
hasPreviousPage,
hasNextPage,
pageLimit,
setPageLimit,
}) => {
const calculatePageOffset = (
currentOffset: number,
total: number,
): string => {
if (total === 0) return '0-0';

const start = currentOffset + 1;
const end = Math.min(total, currentOffset + pageLimit);

return `${start}-${end}`;
};

const calculateTotalPages = (total: number, offset: number): number => {
return Math.ceil(total / pageLimit);
};

const calculateCurrentPage = (offset: number): number => {
return Math.floor(offset / pageLimit) + 1;
};
const itemRange =
totalItems !== undefined && pageSize && totalItems > 1
? `${pageIndex * pageSize + 1}-${Math.min(
totalItems,
(pageIndex + 1) * pageSize,
)}`
: totalItems;
const pageCount =
totalItems !== undefined ? Math.ceil(totalItems / pageSize) : 1;
const hasPreviousPage = pageIndex > 0;
const hasNextPage = totalItems !== undefined && pageIndex < pageCount - 1;

return (
<StyledBoxContainer>
<StyledTypography>
Showing {calculatePageOffset(currentOffset, total)} out of{' '}
{total}
{totalItems !== undefined
? `Showing ${itemRange} item${
totalItems !== 1 ? 's' : ''
} out of ${totalItems}`
: ' '}
</StyledTypography>
<StyledCenterBox>
<ConditionallyRender
Expand All @@ -104,8 +95,7 @@ export const PaginationBar: React.FC<PaginationBarProps> = ({
}
/>
<StyledTypographyPageText>
Page {calculateCurrentPage(currentOffset)} of{' '}
{calculateTotalPages(total, pageLimit)}
Page {pageIndex + 1} of {pageCount}
</StyledTypographyPageText>
<ConditionallyRender
condition={hasNextPage}
Expand All @@ -123,16 +113,16 @@ export const PaginationBar: React.FC<PaginationBarProps> = ({
<StyledCenterBox>
<StyledTypography>Show rows</StyledTypography>

{/* We are using the native select element instead of the Material-UI Select
component due to an issue with Material-UI's Select. When the Material-UI
Select dropdown is opened, it temporarily removes the scrollbar,
causing the page to jump. This can be disorienting for users.
The native select does not have this issue,
as it does not affect the scrollbar when opened.
Therefore, we use the native select to provide a better user experience.
{/* We are using the native select element instead of the Material-UI Select
component due to an issue with Material-UI's Select. When the Material-UI
Select dropdown is opened, it temporarily removes the scrollbar,
causing the page to jump. This can be disorienting for users.
The native select does not have this issue,
as it does not affect the scrollbar when opened.
Therefore, we use the native select to provide a better user experience.
*/}
<StyledSelect
value={pageLimit}
value={pageSize}
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
setPageLimit(Number(event.target.value))
}
Expand Down
Expand Up @@ -31,7 +31,7 @@ interface ICellSortableProps {
isFlex?: boolean;
isFlexGrow?: boolean;
onClick?: MouseEventHandler<HTMLButtonElement>;
styles: React.CSSProperties;
styles?: React.CSSProperties;
}

export const CellSortable: FC<ICellSortableProps> = ({
Expand Down
@@ -1,19 +1,17 @@
import { Box, styled } from '@mui/material';
import { PaginationBar } from 'component/common/PaginationBar/PaginationBar';
import { PaginationBar } from '../PaginationBar/PaginationBar';
import { ComponentProps, FC } from 'react';

const StyledStickyBar = styled('div')(({ theme }) => ({
position: 'sticky',
bottom: 0,
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(2),
marginLeft: theme.spacing(2),
padding: theme.spacing(1.5, 2),
zIndex: theme.zIndex.fab,
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
borderBottomRightRadius: theme.shape.borderRadiusMedium,
borderTop: `1px solid ${theme.palette.divider}`,
boxShadow: `0px -2px 8px 0px rgba(32, 32, 33, 0.06)`,
height: '52px',
}));

const StyledStickyBarContentContainer = styled(Box)(({ theme }) => ({
Expand All @@ -25,12 +23,10 @@ const StyledStickyBarContentContainer = styled(Box)(({ theme }) => ({

export const StickyPaginationBar: FC<ComponentProps<typeof PaginationBar>> = ({
...props
}) => {
return (
<StyledStickyBar>
<StyledStickyBarContentContainer>
<PaginationBar {...props} />
</StyledStickyBarContentContainer>
</StyledStickyBar>
);
};
}) => (
<StyledStickyBar>
<StyledStickyBarContentContainer>
<PaginationBar {...props} />
</StyledStickyBarContentContainer>
</StyledStickyBar>
);
Expand Up @@ -17,6 +17,15 @@ const StyledIconButton = styled(IconButton)(({ theme }) => ({

const StyledIconButtonInactive = styled(StyledIconButton)({
opacity: 0,
'&:hover': {
opacity: 1,
},
'&:focus': {
opacity: 1,
},
'&:active': {
opacity: 1,
},
});

interface IFavoriteIconCellProps {
Expand Down
@@ -1,9 +1,9 @@
import React, { VFC } from 'react';
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
import { FeatureEnvironmentSeen } from 'component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen';
import { FeatureSchema } from 'openapi';

interface IFeatureSeenCellProps {
feature: IFeatureToggleListItem;
feature: FeatureSchema;
}

export const FeatureEnvironmentSeenCell: VFC<IFeatureSeenCellProps> = ({
Expand All @@ -16,7 +16,7 @@ export const FeatureEnvironmentSeenCell: VFC<IFeatureSeenCellProps> = ({

return (
<FeatureEnvironmentSeen
featureLastSeen={feature.lastSeenAt}
featureLastSeen={feature.lastSeenAt || undefined}
environments={environments}
{...rest}
/>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/component/common/Table/index.ts
Expand Up @@ -4,3 +4,4 @@ export { Table } from './Table/Table';
export { TableCell } from './TableCell/TableCell';
export { TablePlaceholder } from './TablePlaceholder/TablePlaceholder';
export { VirtualizedTable } from './VirtualizedTable/VirtualizedTable';
export { PaginatedTable } from './PaginatedTable/PaginatedTable';
Expand Up @@ -3,7 +3,6 @@ import { Box } from '@mui/material';
import { FilterItem } from 'component/common/FilterItem/FilterItem';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useTableState } from 'hooks/useTableState';

export type FeatureTogglesListFilters = {
projectId?: string;
Expand All @@ -25,7 +24,7 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
}));

return (
<Box sx={(theme) => ({ marginBottom: theme.spacing(2) })}>
<Box sx={(theme) => ({ padding: theme.spacing(2, 3) })}>
<ConditionallyRender
condition={projectsOptions.length > 1}
show={() => (
Expand Down

0 comments on commit 755c22f

Please sign in to comment.