From 54c53f21c313389e98001a8d0e69b322da2af12a Mon Sep 17 00:00:00 2001 From: MVarshini Date: Wed, 10 May 2023 00:35:39 +0530 Subject: [PATCH] Support for dynamic pagination (#3386) * PBENCH-732 Pagination for Browsing Page --- dashboard/src/actions/datasetListActions.js | 101 ++++++++++--- dashboard/src/actions/types.js | 8 +- .../assets/constants/browsingPageConstants.js | 10 ++ .../components/DatePickerComponent/index.jsx | 142 ++++++++++-------- .../components/DatePickerComponent/index.less | 5 + .../components/PaginationComponent/index.jsx | 68 +++++++-- .../TableComponent/common-components.jsx | 34 +++-- .../components/TableComponent/index.jsx | 114 +++++++------- .../components/TableComponent/index.less | 3 + dashboard/src/reducers/datasetListReducer.js | 44 ++++-- 10 files changed, 356 insertions(+), 173 deletions(-) create mode 100644 dashboard/src/assets/constants/browsingPageConstants.js diff --git a/dashboard/src/actions/datasetListActions.js b/dashboard/src/actions/datasetListActions.js index 6cce71cbe7..d0d73fad97 100644 --- a/dashboard/src/actions/datasetListActions.js +++ b/dashboard/src/actions/datasetListActions.js @@ -1,27 +1,70 @@ +import * as CONSTANTS from "assets/constants/browsingPageConstants"; import * as TYPES from "./types"; +import { DANGER, ERROR_MSG } from "assets/constants/toastConstants"; + import API from "../utils/axiosInstance"; +import { showToast } from "./toastActions"; import { uriTemplate } from "utils/helper"; -export const fetchPublicDatasets = () => async (dispatch, getState) => { +export const fetchPublicDatasets = (page) => async (dispatch, getState) => { try { dispatch({ type: TYPES.LOADING }); const endpoints = getState().apiEndpoint.endpoints; + const { offset, limit, filter, searchKey, perPage } = + getState().datasetlist; + let publicData = [...getState().datasetlist.publicData]; + const params = new URLSearchParams(); + params.append("metadata", "dataset.uploaded"); + params.append("access", "public"); + params.append("offset", offset); + params.append("limit", limit); + + if (searchKey) { + params.append("name", searchKey); + } + if (filter.startDate instanceof Date && !isNaN(filter.startDate)) { + params.append("start", filter.startDate.toUTCString()); + } + if (filter.endDate instanceof Date && !isNaN(filter.endDate)) { + params.append("end", filter.endDate.toUTCString()); + } + const response = await API.get( uriTemplate(endpoints, "datasets_list", {}), - { params: { metadata: "dataset.uploaded", access: "public" } } + { params } ); + if (response.status === 200 && response.data) { + const startIdx = (page - 1) * perPage; + + if (publicData.length !== response.data.total) { + publicData = new Array(response.data.total); + } + publicData.splice( + startIdx, + response.data.results.length, + ...response.data.results + ); + + dispatch({ + type: TYPES.UPDATE_PUBLIC_DATASETS, + payload: publicData, + }); + // in case of last page, next_url is empty + const offset = response.data.next_url + ? new URLSearchParams(response.data.next_url).get("offset") + : response.data.total; dispatch({ - type: "GET_PUBLIC_DATASETS", - payload: response?.data?.results, + type: TYPES.SET_RESULT_OFFSET, + payload: Number(offset), }); } - dispatch({ type: TYPES.COMPLETED }); - dispatch(callLoading()); } catch (error) { - return error; + dispatch(showToast(DANGER, ERROR_MSG)); + dispatch({ type: TYPES.NETWORK_ERROR }); } + dispatch({ type: TYPES.COMPLETED }); }; export const getFavoritedDatasets = () => async (dispatch) => { @@ -33,23 +76,39 @@ export const getFavoritedDatasets = () => async (dispatch) => { }); }; -export const updateFavoriteRepoNames = (favorites) => async (dispatch) => { - dispatch({ - type: TYPES.FAVORITED_DATASETS, - payload: [...favorites], - }); -}; +export const updateFavoriteRepoNames = (favorites) => ({ + type: TYPES.FAVORITED_DATASETS, + payload: [...favorites], +}); + +export const setPageLimit = (newPerPage) => ({ + type: TYPES.SET_PAGE_LIMIT, + payload: newPerPage, +}); -export const updateTblData = (data) => async (dispatch) => { +export const setFilterKeys = (startDate, endDate) => ({ + type: TYPES.SET_DATE_RANGE, + payload: { startDate, endDate }, +}); + +export const nameFilter = (value) => ({ + type: TYPES.SET_SEARCH_KEY, + payload: value, +}); + +export const applyFilter = () => (dispatch) => { dispatch({ type: TYPES.UPDATE_PUBLIC_DATASETS, - payload: [...data], + payload: [], }); + dispatch({ + type: TYPES.SET_RESULT_OFFSET, + payload: CONSTANTS.INITIAL_RESULT_OFFSET, + }); + dispatch(fetchPublicDatasets(CONSTANTS.START_PAGE_NUMBER)); }; -export const callLoading = () => (dispatch) => { - dispatch({ type: TYPES.LOADING }); - setTimeout(() => { - dispatch({ type: TYPES.COMPLETED }); - }, 5000); -}; +export const setPerPage = (value) => ({ + type: TYPES.SET_PER_PAGE, + payload: value, +}); diff --git a/dashboard/src/actions/types.js b/dashboard/src/actions/types.js index ee559da137..001064790b 100644 --- a/dashboard/src/actions/types.js +++ b/dashboard/src/actions/types.js @@ -22,9 +22,13 @@ export const NAVBAR_OPEN = "NAVBAR_OPEN"; export const NAVBAR_CLOSE = "NAVBAR_CLOSE"; /* PUBLIC DATASETS */ -export const GET_PUBLIC_DATASETS = "GET_PUBLIC_DATASETS"; -export const FAVORITED_DATASETS = "GET_FAVORITE_DATASETS"; export const UPDATE_PUBLIC_DATASETS = "UPDATE_PUBLIC_DATASETS"; +export const FAVORITED_DATASETS = "GET_FAVORITE_DATASETS"; +export const SET_RESULT_OFFSET = "SET_RESULT_OFFSET"; +export const SET_PAGE_LIMIT = "SET_PAGE_LIMIT"; +export const SET_DATE_RANGE = "SET_DATE_RANGE"; +export const SET_SEARCH_KEY = "SET_SEARCH_KEY"; +export const SET_PER_PAGE = "SET_PER_PAGE"; /* DASHBOARD OVERVIEW */ export const USER_RUNS = "USER_RUNS"; diff --git a/dashboard/src/assets/constants/browsingPageConstants.js b/dashboard/src/assets/constants/browsingPageConstants.js new file mode 100644 index 0000000000..a3777d3b04 --- /dev/null +++ b/dashboard/src/assets/constants/browsingPageConstants.js @@ -0,0 +1,10 @@ +export const DEFAULT_PER_PAGE = 20; +export const INITIAL_PAGE_LIMIT = 60; +export const INITIAL_RESULT_OFFSET = 0; +export const OVERFETCH_FACTOR = 3; +export const PER_PAGE_OPTIONS = [ + { title: "10", value: 10 }, + { title: "20", value: 20 }, + { title: "50", value: 50 }, +]; +export const START_PAGE_NUMBER = 1; diff --git a/dashboard/src/modules/components/DatePickerComponent/index.jsx b/dashboard/src/modules/components/DatePickerComponent/index.jsx index ed69ea6c39..b6a72c3678 100644 --- a/dashboard/src/modules/components/DatePickerComponent/index.jsx +++ b/dashboard/src/modules/components/DatePickerComponent/index.jsx @@ -1,77 +1,101 @@ -import React, { useState } from "react"; +import "./index.less"; + +import * as CONSTANTS from "assets/constants/browsingPageConstants"; + import { - InputGroup, - InputGroupText, + Button, DatePicker, + Split, + SplitItem, isValidDate, - Button, } from "@patternfly/react-core"; -import "./index.less"; -import { filterData } from "utils/filterDataset"; -import { - bumpToDate, - dateFromUTCString, - getTodayMidnightUTCDate, -} from "utils/dateFunctions"; +import React, { useState } from "react"; +import { applyFilter, setFilterKeys } from "actions/datasetListActions"; +import { useDispatch, useSelector } from "react-redux"; + +import { getTodayMidnightUTCDate } from "utils/dateFunctions"; + +const DatePickerWidget = (props) => { + const dispatch = useDispatch(); + const { filter } = useSelector((state) => state.datasetlist); + + const [isEndDateError, setIsEndDateError] = useState(false); + + const fromValidator = (date) => + date <= getTodayMidnightUTCDate() + ? "" + : "The Uploaded date cannot be in the future!"; -const DatePickerWidget = ({ - dataArray, - setPublicData, - datasetName, - setDateRange, -}) => { - const [fromDate, setFromDate] = useState({}); - const [toDate, setToDate] = useState( - bumpToDate(getTodayMidnightUTCDate(), 1) - ); - const [strDate, setStrDate] = useState( - new Date().toLocaleDateString("fr-CA") // Return a YYYY-MM-DD string - ); const toValidator = (date) => - date >= fromDate + isValidDate(filter.startDate) && date >= filter.startDate ? "" - : "To date must be greater than or equal to from date"; + : 'The "to" date must be after the "from" date'; - const onFromChange = (_str, date) => { - const selectedDate = dateFromUTCString(_str); - setFromDate(isValidDate(date) ? selectedDate : {}); + const onFromChange = (_event, _str, date) => { + dispatch(setFilterKeys(date, filter.endDate)); + if (filter.endDate) { + checkEndDate(date, filter.endDate); + } else { + setIsEndDateError(true); + } + }; + + const onToChange = (_event, _str, date) => { if (isValidDate(date)) { - if (date > new Date(strDate)) { - setToDate(bumpToDate(dateFromUTCString(_str), 1)); - setStrDate(_str); - } + dispatch(setFilterKeys(filter.startDate, date)); + checkEndDate(filter.startDate, date); + } else { + setIsEndDateError(true); } }; + const checkEndDate = (fromDate, toDate) => + setIsEndDateError(fromDate >= toDate); const filterByDate = () => { - setPublicData(filterData(dataArray, fromDate, toDate, datasetName)); - setDateRange(fromDate, toDate); + if (filter.startDate) { + dispatch(applyFilter()); + props.setPage(CONSTANTS.START_PAGE_NUMBER); + } }; + return ( - - Filter By Date - - to - { - setStrDate(_str); - setToDate(bumpToDate(dateFromUTCString(_str), 1)); - }} - isDisabled={!isValidDate(fromDate)} - rangeStart={fromDate} - validators={[toValidator]} - aria-label="End date" - placeholder="YYYY-MM-DD" - /> - - + <> + + + Filter by date + + + + + to + + + + + + ); }; diff --git a/dashboard/src/modules/components/DatePickerComponent/index.less b/dashboard/src/modules/components/DatePickerComponent/index.less index db4f99a84f..e2d58a86f8 100644 --- a/dashboard/src/modules/components/DatePickerComponent/index.less +++ b/dashboard/src/modules/components/DatePickerComponent/index.less @@ -1,3 +1,8 @@ .filterInputGroup { margin-left: 10px; } +.browsing-page-date-picker { + .pf-c-date-picker__helper-text { + color: #c9190b; + } +} diff --git a/dashboard/src/modules/components/PaginationComponent/index.jsx b/dashboard/src/modules/components/PaginationComponent/index.jsx index 14912ab566..238bf0452c 100644 --- a/dashboard/src/modules/components/PaginationComponent/index.jsx +++ b/dashboard/src/modules/components/PaginationComponent/index.jsx @@ -1,28 +1,74 @@ +import * as TYPES from "actions/types"; + import { Pagination, PaginationVariant } from "@patternfly/react-core"; +import { + fetchPublicDatasets, + setPageLimit, + setPerPage, +} from "actions/datasetListActions"; +import { useDispatch, useSelector } from "react-redux"; + +import { OVERFETCH_FACTOR } from "assets/constants/browsingPageConstants"; import React from "react"; -const TablePagination = ({ - numberOfRows, - page, - setPage, - perPage, - setPerPage, -}) => { +const TablePagination = ({ page, setPage }) => { + const dispatch = useDispatch(); + + const { publicData, perPage } = useSelector((state) => state.datasetlist); const onSetPage = (_event, pageNumber) => { setPage(pageNumber); + + fetchData(_event, pageNumber, perPage); }; - const onPerPageSelect = (_event, perPage) => { - setPerPage(perPage); + const onPerPageSelect = (_event, newPerPage, newPage) => { + dispatch(setPageLimit(newPerPage * OVERFETCH_FACTOR)); + + dispatch(setPerPage(newPerPage)); + setPage(newPage); + + fetchData(_event, newPage, newPerPage); }; + const fetchData = (_event, newPage, newPerPage = perPage) => { + const startIdx = (newPage - 1) * newPerPage; + let left = startIdx; + let right = startIdx + newPerPage - 1; + while (left < right) { + if (publicData[left]) { + left++; + } else { + break; + } + if (publicData[right]) { + right--; + } else { + break; + } + } + if (left !== right) { + dispatch({ + type: TYPES.SET_RESULT_OFFSET, + payload: startIdx, + }); + dispatch(fetchPublicDatasets(newPage)); + } + }; + return ( ( + + {firstIndex} - {lastIndex} of {publicData.length} + + )} > ); }; diff --git a/dashboard/src/modules/components/TableComponent/common-components.jsx b/dashboard/src/modules/components/TableComponent/common-components.jsx index 805159b758..4cf2ebfa1b 100644 --- a/dashboard/src/modules/components/TableComponent/common-components.jsx +++ b/dashboard/src/modules/components/TableComponent/common-components.jsx @@ -1,5 +1,7 @@ import "./index.less"; +import * as CONSTANTS from "assets/constants/browsingPageConstants"; + import { Alert, AlertActionCloseButton, @@ -16,9 +18,10 @@ import { Title, } from "@patternfly/react-core"; import React, { useState } from "react"; +import { applyFilter, nameFilter } from "actions/datasetListActions"; import SearchIcon from "@patternfly/react-icons/dist/esm/icons/search-icon"; -import { filterData } from "utils/filterDataset"; +import { useDispatch } from "react-redux"; import { useOutletContext } from "react-router-dom"; export const LoginHint = (props) => { @@ -66,18 +69,13 @@ export const Heading = (props) => { ); }; -export const SearchBox = ({ - dataArray, - setPublicData, - startDate, - endDate, - setDatasetName, -}) => { +export const SearchBox = (props) => { const [searchKey, setSearchKey] = useState(""); - + const dispatch = useDispatch(); const search = () => { - setPublicData(filterData(dataArray, startDate, endDate, searchKey)); - setDatasetName(searchKey); + dispatch(nameFilter(searchKey)); + dispatch(applyFilter()); + props.setPage(CONSTANTS.START_PAGE_NUMBER); }; const handleKeyPress = (e) => { const key = e.key; @@ -88,13 +86,19 @@ export const SearchBox = ({ return ( handleKeyPress(e)} - onChange={(e) => setSearchKey(e)} - > - diff --git a/dashboard/src/modules/components/TableComponent/index.jsx b/dashboard/src/modules/components/TableComponent/index.jsx index e6a745d524..6dcb0cce2d 100644 --- a/dashboard/src/modules/components/TableComponent/index.jsx +++ b/dashboard/src/modules/components/TableComponent/index.jsx @@ -1,6 +1,7 @@ import "./index.less"; import * as APP_ROUTES from "utils/routeConstants"; +import * as CONSTANTS from "assets/constants/browsingPageConstants"; import { EmptyTable, Heading, LoginHint, SearchBox } from "./common-components"; import { @@ -15,26 +16,21 @@ import { } from "@patternfly/react-table"; import React, { useEffect, useState } from "react"; import { ToggleGroup, ToggleGroupItem } from "@patternfly/react-core"; -import { bumpToDate, getTodayMidnightUTCDate } from "utils/dateFunctions"; import { fetchPublicDatasets, getFavoritedDatasets, updateFavoriteRepoNames, - updateTblData, } from "actions/datasetListActions"; import { useDispatch, useSelector } from "react-redux"; import { DATASET_UPLOADED } from "assets/constants/overviewConstants"; import DatePickerWidget from "../DatePickerComponent"; import PathBreadCrumb from "../BreadCrumbComponent"; +import { RenderPagination } from "../OverviewComponent/common-component"; import { TOC } from "assets/constants/navigationConstants"; import TablePagination from "../PaginationComponent"; -import { useNavigate } from "react-router"; import { useKeycloak } from "@react-keycloak/web"; - -let startDate = new Date(Date.UTC(1990, 10, 4)); -let endDate = bumpToDate(getTodayMidnightUTCDate(), 1); -let datasetName = ""; +import { useNavigate } from "react-router"; const TableWithFavorite = () => { const columnNames = { @@ -46,26 +42,30 @@ const TableWithFavorite = () => { const [activeSortIndex, setActiveSortIndex] = useState(null); const [activeSortDirection, setActiveSortDirection] = useState(null); const [isSelected, setIsSelected] = useState("datasetListButton"); - const [page, setPage] = useState(1); - const [perPage, setPerPage] = useState(10); + const [loginHintVisible, setLoginHintVisible] = useState(true); + + const [favTblperPage, setfavTblPerPage] = useState( + CONSTANTS.DEFAULT_PER_PAGE + ); + const [favPage, setFavPage] = useState(CONSTANTS.START_PAGE_NUMBER); + const [page, setPage] = useState(CONSTANTS.START_PAGE_NUMBER); + const navigate = useNavigate(); const dispatch = useDispatch(); useEffect(() => { if (Object.keys(endpoints).length > 0) { - dispatch(fetchPublicDatasets()); + dispatch(fetchPublicDatasets(CONSTANTS.START_PAGE_NUMBER)); dispatch(getFavoritedDatasets()); } }, [dispatch, endpoints]); - const { publicData, favoriteRepoNames, tableData } = useSelector( + const { publicData, favoriteRepoNames, perPage } = useSelector( (state) => state.datasetlist ); - const setPublicData = (data) => { - dispatch(updateTblData(data)); - }; + const markRepoFavorited = (repo, isFavoriting = true) => { const otherFavorites = favoriteRepoNames.filter( (r) => r.name !== repo.name @@ -76,13 +76,17 @@ const TableWithFavorite = () => { saveFavorites(newFavorite); dispatch(updateFavoriteRepoNames(newFavorite)); }; - const selectedArray = + + let selectedArray = isSelected === "datasetListButton" ? publicData?.slice((page - 1) * perPage, page * perPage) - : favoriteRepoNames?.slice((page - 1) * perPage, page * perPage); + : favoriteRepoNames?.slice( + (favPage - 1) * favTblperPage, + favPage * favTblperPage + ); const isRepoFavorited = (repo) => - !!favoriteRepoNames.find((element) => element.name === repo.name); + !!favoriteRepoNames?.find((element) => element?.name === repo?.name); const getSortableRowValues = (data) => { const uploadedDate = data.metadata[DATASET_UPLOADED]; @@ -118,13 +122,7 @@ const TableWithFavorite = () => { const id = event.currentTarget.id; setIsSelected(id); }; - const setDatasetName = (datasetNameValue) => { - datasetName = datasetNameValue; - }; - const setDateRange = (startDateValue, endDateValue) => { - startDate = startDateValue; - endDate = endDateValue; - }; + const saveFavorites = (fav) => { localStorage.setItem("favorite_datasets", JSON.stringify(fav)); }; @@ -137,6 +135,18 @@ const TableWithFavorite = () => { { name: "Results", link: "" }, ]; + /* Favorite Table Pagination */ + const onSetPage = (_evt, newPage, _perPage, startIdx, endIdx) => { + setFavPage(newPage); + selectedArray = favoriteRepoNames?.slice(startIdx, endIdx); + }; + + const onPerPageSelect = (_evt, newPerPage, newPage, startIdx, endIdx) => { + setfavTblPerPage(newPerPage); + setFavPage(newPage); + selectedArray = favoriteRepoNames?.slice(startIdx, endIdx); + }; + /* Favorite Table Pagination*/ return ( <> {!keycloak.authenticated && loginHintVisible && ( @@ -154,22 +164,11 @@ const TableWithFavorite = () => { headingTitle="Results" >
- - + + +
+ { {selectedArray.length > 0 ? ( selectedArray.map((repo, rowIndex) => ( - + navigate(`${TOC}/${repo.resource_id}`)} + onClick={() => + navigate(`${TOC}/${repo?.resource_id}`) + } > - {repo.name} + {repo?.name} - {repo.metadata[DATASET_UPLOADED]} + {repo?.metadata[DATASET_UPLOADED]} { )) ) : ( - + @@ -236,17 +237,20 @@ const TableWithFavorite = () => { )} - + {isSelected === "datasetListButton" ? ( + + ) : ( + + )} diff --git a/dashboard/src/modules/components/TableComponent/index.less b/dashboard/src/modules/components/TableComponent/index.less index 278bd76485..2c73f66cfe 100644 --- a/dashboard/src/modules/components/TableComponent/index.less +++ b/dashboard/src/modules/components/TableComponent/index.less @@ -39,3 +39,6 @@ table th { .searchInputGroup { width: 25vw !important; } +.filter-btn { + height: 36px; +} diff --git a/dashboard/src/reducers/datasetListReducer.js b/dashboard/src/reducers/datasetListReducer.js index 33a95b3aaa..5f8540a339 100644 --- a/dashboard/src/reducers/datasetListReducer.js +++ b/dashboard/src/reducers/datasetListReducer.js @@ -1,33 +1,57 @@ -import { - GET_PUBLIC_DATASETS, - FAVORITED_DATASETS, - UPDATE_PUBLIC_DATASETS, -} from "../actions/types"; +import * as CONSTANTS from "assets/constants/browsingPageConstants"; +import * as TYPES from "actions/types"; const initialState = { publicData: [], favoriteRepoNames: [], tableData: [], + offset: CONSTANTS.INITIAL_RESULT_OFFSET, + limit: CONSTANTS.INITIAL_PAGE_LIMIT, + perPage: CONSTANTS.DEFAULT_PER_PAGE, + searchKey: "", + filter: { + startDate: "", + endDate: "", + }, }; const DatasetListReducer = (state = initialState, action = {}) => { const { type, payload } = action; switch (type) { - case GET_PUBLIC_DATASETS: + case TYPES.UPDATE_PUBLIC_DATASETS: return { ...state, publicData: [...payload], - tableData: [...payload], }; - case FAVORITED_DATASETS: + case TYPES.FAVORITED_DATASETS: return { ...state, favoriteRepoNames: [...payload], }; - case UPDATE_PUBLIC_DATASETS: + case TYPES.SET_RESULT_OFFSET: return { ...state, - publicData: [...payload], + offset: payload, + }; + case TYPES.SET_PAGE_LIMIT: + return { + ...state, + limit: payload, + }; + case TYPES.SET_DATE_RANGE: + return { + ...state, + filter: payload, + }; + case TYPES.SET_SEARCH_KEY: + return { + ...state, + searchKey: payload, + }; + case TYPES.SET_PER_PAGE: + return { + ...state, + perPage: payload, }; default: return state;