Skip to content

Commit

Permalink
Merge pull request #1547 from thehyve/FAIRSPC-108_metadata_page_count
Browse files Browse the repository at this point in the history
Fairspc 108 metadata page count
  • Loading branch information
tgreenwood authored Aug 12, 2024
2 parents 1b3746e + d2fdb00 commit 0b6be79
Show file tree
Hide file tree
Showing 18 changed files with 468 additions and 183 deletions.
2 changes: 1 addition & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1239,7 +1239,7 @@ curl -X POST -H 'Content-type: application/json' -H 'Accept: application/json' -
|===
3+| ``POST /api/views/count``

3+| Count rows of a view matching request filters.
3+| Count rows of a view matching request filters. If `maxDisplayCount` configured in the `views.yaml` for a view, then the count for the view is limited by this value if total count exceeds it. Otherwise, the total count is returned.
3+| _Parameters:_
| ``view``
| string
Expand Down
30 changes: 18 additions & 12 deletions projects/mercury/src/common/components/TablePaginationActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import IconButton from '@mui/material/IconButton';
import {KeyboardArrowLeft, KeyboardArrowRight, LastPage, FirstPage} from '@mui/icons-material';
import makeStyles from '@mui/styles/makeStyles';
import {Typography} from '@mui/material';
import {Tooltip, Typography} from '@mui/material';

const useStyles = makeStyles(theme => ({
root: {
Expand All @@ -15,12 +15,14 @@ export type TablePaginationActionsProperties = {
count: number,
onPageChange: () => {},
page: number,
rowsPerPage: number
rowsPerPage: number,
countDisplayLimitReached?: boolean,
currentPageCount?: number
};

const TablePaginationActions = (props: TablePaginationActionsProperties) => {
const classes = useStyles();
const {count, page, rowsPerPage, onPageChange} = props;
const {count, page, rowsPerPage, onPageChange, countDisplayLimitReached = false, hasNextFlag = false} = props;

const handleFirstPageButtonClick = event => {
onPageChange(event, 0);
Expand Down Expand Up @@ -56,20 +58,24 @@ const TablePaginationActions = (props: TablePaginationActionsProperties) => {
</Typography>
<IconButton
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
disabled={page >= Math.ceil(count / rowsPerPage) - 1 && !(countDisplayLimitReached && hasNextFlag)}
aria-label="next page"
size="medium"
>
<KeyboardArrowRight />
</IconButton>
<IconButton
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
size="medium"
>
<LastPage />
</IconButton>
<Tooltip title={countDisplayLimitReached ? 'Total page count not available' : ''}>
<span>
<IconButton
onClick={handleLastPageButtonClick}
disabled={countDisplayLimitReached || page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
size="medium"
>
<LastPage />
</IconButton>
</span>
</Tooltip>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react';
import {render, fireEvent} from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import {ThemeProvider} from '@mui/material/styles';
import TablePaginationActions from '../TablePaginationActions';
import theme from '../../../App.theme';

describe('TablePaginationActions', () => {
const defaultProps = {
count: 100,
page: 0,
rowsPerPage: 10,
onPageChange: jest.fn()
};

const renderComponent = (props = {}) => {
return render(
<ThemeProvider theme={theme}>
<TablePaginationActions {...defaultProps} {...props} />
</ThemeProvider>
);
};

it('renders all arrow buttons', () => {
const {getByLabelText} = renderComponent();
expect(getByLabelText('first page')).toBeInTheDocument();
expect(getByLabelText('previous page')).toBeInTheDocument();
expect(getByLabelText('next page')).toBeInTheDocument();
expect(getByLabelText('last page')).toBeInTheDocument();
});

it('disables next and last buttons on last page', () => {
const props = {...defaultProps, page: 9};
const {getByLabelText} = renderComponent(props);
expect(getByLabelText('next page')).toBeDisabled();
expect(getByLabelText('last page')).toBeDisabled();
});

it('calls onPageChange with correct arguments when first page button is clicked', () => {
const props = {...defaultProps, page: 1};
const {getByLabelText} = renderComponent(props);
fireEvent.click(getByLabelText('first page'));
expect(defaultProps.onPageChange).toHaveBeenCalledWith(expect.anything(), 0);
});
it('calls onPageChange with correct arguments when previous page button is clicked', () => {
const props = {...defaultProps, page: 1};
const {getByLabelText} = renderComponent(props);
fireEvent.click(getByLabelText('previous page'));
expect(props.onPageChange).toHaveBeenCalledWith(expect.anything(), 0);
});

it('calls onPageChange with correct arguments when next page button is clicked', () => {
const {getByLabelText} = renderComponent();
fireEvent.click(getByLabelText('next page'));
expect(defaultProps.onPageChange).toHaveBeenCalledWith(expect.anything(), 1);
});

it('calls onPageChange with correct arguments when last page button is clicked', () => {
const {getByLabelText} = renderComponent();
fireEvent.click(getByLabelText('last page'));
expect(defaultProps.onPageChange).toHaveBeenCalledWith(expect.anything(), 9);
});

it('disables last page button when countDisplayLimitReached is true', () => {
const props = {...defaultProps, countDisplayLimitReached: true};
const {getByLabelText} = renderComponent(props);
expect(getByLabelText('last page')).toBeDisabled();
});

it('shows tooltip when countDisplayLimitReached is true', () => {
const props = {countDisplayLimitReached: true};
const {getByLabelText} = renderComponent(props);
fireEvent.mouseOver(getByLabelText('last page'));
expect(getByLabelText('Total page count not available')).toBeInTheDocument();
});

it('enables next page button when countDisplayLimitReached is true and hasNextFlag is true', () => {
const props = {...defaultProps, countDisplayLimitReached: true, hasNextFlag: true};
const {getByLabelText} = renderComponent(props);
expect(getByLabelText('next page')).not.toBeDisabled();
});
});
3 changes: 2 additions & 1 deletion projects/mercury/src/metadata/views/MetadataViewAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export type MetadataViewColumn = {
export type MetadataViewOptions = {
name: string,
title: string,
columns: MetadataViewColumn[]
columns: MetadataViewColumn[],
maxDisplayCount: number
};

export type MetadataViews = {
Expand Down
63 changes: 51 additions & 12 deletions projects/mercury/src/metadata/views/MetadataViewTableContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '@mui/material';
import withStyles from '@mui/styles/withStyles';
import {useHistory} from 'react-router-dom';
import {Addchart, ViewColumn, Check} from '@mui/icons-material';
import {Addchart, ViewColumn, Check, ErrorOutline} from '@mui/icons-material';
import Checkbox from '@mui/material/Checkbox';
import FormControl from '@mui/material/FormControl';
import Popover from '@mui/material/Popover';
Expand Down Expand Up @@ -46,6 +46,7 @@ type MetadataViewTableContainerProperties = {
setTextFiltersObject: () => {},
toggleRow: () => {},
view: string,
viewCountDisplayLimit: number,
collections: Collection[],
locationContext: string,
selected: MetadataViewEntityWithLinkedFiles,
Expand All @@ -68,6 +69,10 @@ const styles = theme => ({
fontSize: 12,
padding: 2
},
countErrorIcon: {
fontSize: 16,
marginLeft: 3
},
tableContents: {
minHeight: '200px',
maxHeight: 'calc(100vh - 250px)',
Expand All @@ -87,19 +92,24 @@ const styles = theme => ({
float: 'right',
maxWidth: 50
},
tablePaginationLabel: {
fontWeight: 'bold',
alignContent: 'center'
},
viewColumnsFormControl: {
padding: 10
},
messageBox: {
padding: 5
padding: 0
}
});

const LOCAL_STORAGE_METADATA_TABLE_ROWS_NUM_KEY = 'FAIRSPACE_METADATA_TABLE_ROWS_NUM';
const SESSION_STORAGE_VISIBLE_COLUMNS_KEY_PREFIX = 'FAIRSPACE_METADATA_VISIBLE_COLUMNS';

export const MetadataViewTableContainer = (props: MetadataViewTableContainerProperties) => {
const {view, filters, columns, idColumn, hasInactiveFilters, locationContext, classes} = props;
const {view, viewCountDisplayLimit, filters, columns, idColumn, hasInactiveFilters, locationContext, classes} =
props;
const {textFiltersObject, setTextFiltersObject} = props;

const {isFeatureEnabled} = useContext(FeaturesContext);
Expand All @@ -119,13 +129,14 @@ export const MetadataViewTableContainer = (props: MetadataViewTableContainerProp
const columnSelectorOpen = Boolean(anchorEl);
const history = useHistory();

const {data, count, error, loading, loadingCount, refreshDataOnly} = useViewData(
const {data, count, error, countError, loading, loadingCount, refreshDataOnly} = useViewData(
view,
filters,
textFiltersObject,
locationContext,
rowsPerPage
);
const viewCountDisplayLimitReached = viewCountDisplayLimit !== undefined && count.count === viewCountDisplayLimit;
const [rowCheckboxes, setRowCheckboxes] = React.useState({});

const resetRowCheckboxes = () => {
Expand Down Expand Up @@ -243,13 +254,21 @@ export const MetadataViewTableContainer = (props: MetadataViewTableContainerProp

const renderMessages = () => (
<div className={classes.messageBox}>
{count.timeout && <MessageDisplay small message="The count request timed out." />}
{count.timeout && (
<MessageDisplay
color="warning"
messageColor={theme.palette.primary.main}
small
message="The count request timed out."
/>
)}
{hasInactiveFilters && (
<MessageDisplay
color="primary"
isError={false}
small
message="Apply filters to see data matching your current selection."
messageColor={theme.palette.primary.main}
/>
)}
</div>
Expand Down Expand Up @@ -301,14 +320,26 @@ export const MetadataViewTableContainer = (props: MetadataViewTableContainerProp
</div>
);

const labelDisplayedRows = ({from, to, count: totalCount, countIsLoading}) => (
const getTotalCountString = (totalCount: number, to: number) => {
if (totalCount === undefined) return '...';
if (totalCount === -1) return `more than ${to}`;
if (viewCountDisplayLimitReached) return `more than ${viewCountDisplayLimit - 1}`;
return totalCount;
};

const renderTablePaginationLabel = ({from, to, countIsLoading, countHasError}) => (
<span>
<Typography variant="body2" component="span" display="inline">
{from}-{to} of{' '}
</Typography>
<Typography variant="body2" component="span" display="inline" style={{fontWeight: 'bold'}}>
{totalCount !== undefined && totalCount !== -1 ? totalCount.toLocaleString() : 'more than ' + to}
<Typography variant="body2" component="span" display="inline" className={classes.tablePaginationLabel}>
{getTotalCountString(count?.count, to)}
{countIsLoading && <CircularProgress size={14} style={{marginLeft: 3}} />}
{(countHasError || count?.timeout) && (
<Tooltip title="Error occured when counting results">
<ErrorOutline size="small" className={classes.countErrorIcon} color="warning" />
</Tooltip>
)}
</Typography>
</span>
);
Expand All @@ -318,7 +349,7 @@ export const MetadataViewTableContainer = (props: MetadataViewTableContainerProp
return <MessageDisplay message={error.message} />;
}

if (count.count === 0 && !data.timeout && !count.timeout) {
if (count.count === 0 && !data.timeout) {
return <MessageDisplay message="No results found." />;
}
if (data && data.timeout) {
Expand All @@ -344,7 +375,7 @@ export const MetadataViewTableContainer = (props: MetadataViewTableContainerProp
setPage(0);
}, [filters]);

useEffect(() => {
useDeepCompareEffect(() => {
resetRowCheckboxes();
}, [data]);

Expand Down Expand Up @@ -422,8 +453,16 @@ export const MetadataViewTableContainer = (props: MetadataViewTableContainerProp
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
className={classes.tableFooter}
labelDisplayedRows={d => labelDisplayedRows({...d, countIsLoading: loadingCount})}
ActionsComponent={TablePaginationActions}
labelDisplayedRows={d =>
renderTablePaginationLabel({...d, countIsLoading: loadingCount, countHasError: countError})
}
ActionsComponent={p =>
TablePaginationActions({
...p,
countDisplayLimitReached: viewCountDisplayLimitReached,
hasNextFlag: data?.hasNext
})
}
/>
</div>
</LoadingOverlayWrapper>
Expand Down
1 change: 1 addition & 0 deletions projects/mercury/src/metadata/views/MetadataViewTabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const MetadataViewTabs = (props: MetadataViewTabsProperties) => {
columns={appendCustomColumns(view)}
idColumn={idColumn}
view={view.name}
viewCountDisplayLimit={view.maxDisplayCount}
filters={filters}
locationContext={locationContext}
selected={selected}
Expand Down
24 changes: 16 additions & 8 deletions projects/mercury/src/metadata/views/UseViewData.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const useViewData = (
const [loading, setLoading] = useState(true);
const [loadingCount, setLoadingCount] = useState(true);
const [error, setError] = useState();
const [countError, setCountError] = useState();
const [countRequestCancelToken, setCountRequestCancelToken] = useState();
const [viewDataRequestCancelToken, setViewDataRequestCancelToken] = useState();

Expand Down Expand Up @@ -63,15 +64,21 @@ const useViewData = (
}
const token = axios.CancelToken.source();
setCountRequestCancelToken(token);
metadataViewAPI.getCount(token, view, allFilters).then(res => {
if (res) {
if (res.count == null) {
res.count = -1;
metadataViewAPI
.getCount(token, view, allFilters)
.then(res => {
if (res) {
if (res.count == null) {
res.count = -1;
}
setCount(res);
}
setCount(res);
setLoadingCount(false);
}
});
})
.catch(e => {
setCountError(e || true);
console.error(e || new Error('Error while fetching counts.'));
})
.finally(() => setLoadingCount(false));
};

const fetchViewData = (newPage: number, newRowsPerPage: number): Promise<MetadataViewData> => {
Expand Down Expand Up @@ -133,6 +140,7 @@ const useViewData = (
loading,
loadingCount,
error,
countError,
refreshDataOnly
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ public Services(

queryService = viewStoreClientFactory == null
? new SparqlQueryService(config.search, viewsConfig, filteredDataset)
: new JdbcQueryService(config.search, viewStoreClientFactory, transactions, davFactory.root);
: new JdbcQueryService(
config.search, viewsConfig, viewStoreClientFactory, transactions, davFactory.root);
viewService =
new ViewService(config, viewsConfig, filteredDataset, viewStoreClientFactory, metadataPermissions);

Expand Down
Loading

0 comments on commit 0b6be79

Please sign in to comment.