diff --git a/package-lock.json b/package-lock.json index 2965c40..749bb05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "react-datepicker": "^4.2.1", "react-dom": "^17.0.2", "react-intl": "^5.20.12", + "react-placeholder": "^4.1.0", "react-query": "^3.27.0", "react-redux": "^7.2.5", "react-router-dom": "^5.3.0", @@ -18483,6 +18484,14 @@ "react-dom": "^15.5.x || ^16.x || ^17.x" } }, + "node_modules/react-placeholder": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/react-placeholder/-/react-placeholder-4.1.0.tgz", + "integrity": "sha512-z1HGD86NWJTYTQumHsmGH9jkozv4QHa9dju/vHVUd4f1svu23pf5v7QoBLBfs3kA1S9GLJaCeRMHLbO2SCdz5A==", + "peerDependencies": { + "react": "^16.8.0 || ^17" + } + }, "node_modules/react-popper": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.5.tgz", @@ -38342,6 +38351,12 @@ "integrity": "sha512-oPlOTYcISLHfpMog2lUZMFSbqOs4LFcA4+vo7fpfevB5v9Z0D5VBDBkfeO5lv+hpEcGoaGk67braLT+QT+eICA==", "requires": {} }, + "react-placeholder": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/react-placeholder/-/react-placeholder-4.1.0.tgz", + "integrity": "sha512-z1HGD86NWJTYTQumHsmGH9jkozv4QHa9dju/vHVUd4f1svu23pf5v7QoBLBfs3kA1S9GLJaCeRMHLbO2SCdz5A==", + "requires": {} + }, "react-popper": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.5.tgz", diff --git a/package.json b/package.json index a37ce23..2c1051a 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "react-datepicker": "^4.2.1", "react-dom": "^17.0.2", "react-intl": "^5.20.12", + "react-placeholder": "^4.1.0", "react-query": "^3.27.0", "react-redux": "^7.2.5", "react-router-dom": "^5.3.0", diff --git a/src/assets/svgIcons/index.js b/src/assets/svgIcons/index.js index 4af57cd..965f1b0 100644 --- a/src/assets/svgIcons/index.js +++ b/src/assets/svgIcons/index.js @@ -1,2 +1,3 @@ export { BanIcon } from "./ban"; export { SortIcon, SortDownIcon, SortUpIcon } from "./sortIcons"; +export { SpinnerIcon } from "./spinner"; diff --git a/src/assets/svgIcons/spinner.js b/src/assets/svgIcons/spinner.js new file mode 100644 index 0000000..c09c0d6 --- /dev/null +++ b/src/assets/svgIcons/spinner.js @@ -0,0 +1,22 @@ +import React from "react"; + +// Icon produced by FontAwesome project: https://github.com/FortAwesome/Font-Awesome/ +// License: CC-By 4.0 +export class SpinnerIcon extends React.PureComponent { + render() { + return ( + <svg + aria-hidden="true" + focusable="false" + role="img" + viewBox="0 0 512 512" + {...this.props} + > + <path + fill="currentColor" + d="M222.7 32.15C227.7 49.08 218.1 66.9 201.1 71.94C121.8 95.55 64 169.1 64 255.1C64 362 149.1 447.1 256 447.1C362 447.1 448 362 448 255.1C448 169.1 390.2 95.55 310.9 71.94C293.9 66.9 284.3 49.08 289.3 32.15C294.4 15.21 312.2 5.562 329.1 10.6C434.9 42.07 512 139.1 512 255.1C512 397.4 397.4 511.1 256 511.1C114.6 511.1 0 397.4 0 255.1C0 139.1 77.15 42.07 182.9 10.6C199.8 5.562 217.6 15.21 222.7 32.15V32.15z" + /> + </svg> + ); + } +} diff --git a/src/components/download.js b/src/components/download.js index 1aa0b45..09044fd 100644 --- a/src/components/download.js +++ b/src/components/download.js @@ -105,7 +105,9 @@ export const DownloadCSVButton = ({ data, source }) => { )}`; return ( - <div className="text-right m-4 text-lg"> + <div + className={`text-right text-lg ${source === "mapathon" ? "m-4" : "my-4"}`} + > <CSVLink data={data} headers={headers} diff --git a/src/components/userGroup/userGroupResults.js b/src/components/userGroup/userGroupResults.js index 33d3e31..7b16fd8 100644 --- a/src/components/userGroup/userGroupResults.js +++ b/src/components/userGroup/userGroupResults.js @@ -2,13 +2,25 @@ import React from "react"; import { useSelector } from "react-redux"; import { useTable, useSortBy } from "react-table"; import { FormattedMessage } from "react-intl"; +import ReactPlaceholder from "react-placeholder"; import messages from "./messages"; import { Error } from "../formResponses"; import { UserGroupErrorMessage } from "./userGroupError"; -import { SortDownIcon, SortIcon, SortUpIcon } from "../../assets/svgIcons"; +import { + SortDownIcon, + SortIcon, + SortUpIcon, + SpinnerIcon, +} from "../../assets/svgIcons"; import { DownloadCSVButton } from "../download"; +import "react-placeholder/lib/reactPlaceholder.css"; -export function UserGroupResultsTable({ columns, data, userDataCheck }) { +export function UserGroupResultsTable({ + columns, + data, + userDataCheck, + loading, +}) { const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable( { @@ -28,63 +40,82 @@ export function UserGroupResultsTable({ columns, data, userDataCheck }) { <UserGroupErrorMessage error={downloadError} /> </Error> )} - <DownloadCSVButton data={data} source={"userGroup"} /> <div className="flex flex-col"> - <div className="overflow-x-auto sm:-mx-6 lg:-mx-8"> - <div className="py-2 inline-block min-w-full sm:px-6 lg:px-8"> - <div className="overflow-x-auto"></div> - <table {...getTableProps()} className="min-w-full"> - <thead className="border-b"> - {headerGroups.map((headerGroup) => ( - <tr {...headerGroup.getHeaderGroupProps()}> - {headerGroup.headers.map((column) => ( - <th - scope="col" - className="text-xl font-semibold px-6 py-4 text-left" - {...column.getHeaderProps( - column.getSortByToggleProps() - )} - > - <span className="inline "> - {column.render("Header")} - {column.canSort && - (column.isSorted ? ( - column.isSortedDesc ? ( - <SortDownIcon className="w-3 h-3 ml-1 inline text-blue-grey" /> + <ReactPlaceholder + showLoadingAnimation + ready={rows.length > 0} + type="text" + rows={8} + className="mt-20 mx-4" + > + <div className="w-11/12 mx-auto"> + <DownloadCSVButton data={data} source={"userGroup"} /> + </div> + <div className="overflow-x-auto sm:-mx-6 lg:-mx-8"> + <div className="py-2 inline-block min-w-full sm:px-6 lg:px-8"> + <div className="overflow-x-auto" /> + <table {...getTableProps()} className="w-11/12 mx-auto border"> + <thead className="border-b"> + {headerGroups.map((headerGroup) => ( + <tr {...headerGroup.getHeaderGroupProps()}> + {headerGroup.headers.map((column) => ( + <th + scope="col" + className="text-xl font-semibold p-4 text-center" + {...column.getHeaderProps( + column.getSortByToggleProps() + )} + > + <span className="inline "> + {column.render("Header")} + {column.canSort && + (column.isSorted ? ( + column.isSortedDesc ? ( + <SortDownIcon className="w-3 h-3 ml-1 inline text-blue-grey" /> + ) : ( + <SortUpIcon className="w-3 h-3 ml-1 inline text-blue-grey" /> + ) ) : ( - <SortUpIcon className="w-3 h-3 ml-1 inline text-blue-grey" /> - ) - ) : ( - <SortIcon className="w-3 h-3 ml-1 inline text-blue-grey" /> - ))} - </span> - </th> - ))} - </tr> - ))} - </thead> - <tbody {...getTableBodyProps()}> - {rows.map((row, i) => { - prepareRow(row); - return ( - <tr {...row.getRowProps()} className="border-b"> - {row.cells.map((cell) => { - return ( - <td - className="text-lg font-light px-6 py-4 whitespace-nowrap" - {...cell.getCellProps()} - > - {cell.render("Cell")} - </td> - ); - })} + <SortIcon className="w-3 h-3 ml-1 inline text-blue-grey" /> + ))} + </span> + </th> + ))} </tr> - ); - })} - </tbody> - </table> + ))} + </thead> + <tbody {...getTableBodyProps()}> + {rows.map((row, i) => { + prepareRow(row); + return ( + <tr {...row.getRowProps()} className="border-b" key={i}> + {row.cells.map((cell) => { + return ( + <td + className="text-lg font-light p-4 text-center whitespace-nowrap" + {...cell.getCellProps()} + > + {cell.render("Cell")} + </td> + ); + })} + </tr> + ); + })} + </tbody> + </table> + {loading ? ( + <div className="text-center w-11/12 mx-auto"> + <SpinnerIcon className="animate-spin w-5 h-5 mt-2 inline text-red" /> + </div> + ) : ( + <div className="bg-tan w-11/12 text-center mx-auto"> + End of Table + </div> + )} + </div> </div> - </div> + </ReactPlaceholder> </div> </> ); diff --git a/src/utils/userGroupUtils.js b/src/utils/userGroupUtils.js index d7bcc2f..eb982dc 100644 --- a/src/utils/userGroupUtils.js +++ b/src/utils/userGroupUtils.js @@ -1,19 +1,12 @@ -export const featureActionCount = (array, feature, action) => { - let x = array.filter( - (i) => i["feature"] === feature && i["action"] === action - ); - return x[0] ? x[0]["count"] : 0; -}; - export const aggregateUserGroupData = (obj) => { const arr = []; obj.forEach((i) => { arr.push({ ...i, - createdBuildings: featureActionCount(i["stats"], "building", "create"), - modifiedBuildings: featureActionCount(i["stats"], "building", "modify"), - createdHighways: featureActionCount(i["stats"], "highway", "create"), - modifiedHighways: featureActionCount(i["stats"], "highway", "modify"), + createdBuildings: i["stats"][0]["addedBuildings"], + modifiedBuildings: i["stats"][0]["modifiedBuildings"], + createdHighways: i["stats"][0]["addedHighway"], + modifiedHighways: i["stats"][0]["modifiedHighway"], }); }); return arr; diff --git a/src/views/MapathonReports.js b/src/views/MapathonReports.js index 57d3fa2..1f20e10 100644 --- a/src/views/MapathonReports.js +++ b/src/views/MapathonReports.js @@ -15,6 +15,7 @@ import { setTriggerSubmit } from "../features/form/formSlice"; import { MiniNavBar } from "../components/nav/navbar"; import { aggregateMapathonUserData } from "../utils/mapathonDataUtils"; import { MapathonDetailedTableHeaders } from "../components/mapathon/constants"; +import { SpinnerIcon } from "../assets/svgIcons"; const MAPATHON_PAGES = [ { pageTitle: "Mapathon Summary Report", pageURL: "/mapathon-report/summary" }, @@ -51,7 +52,10 @@ export const MapathonSummaryReport = (props) => { <MapathonRedirectButton triggerFn={triggeredSubmit} /> </div> {isLoading && ( - <div className="mx-auto text-center w-1/4 p-1 mt-5">Loading...</div> + <div className="mx-auto text-center w-1/4 p-1 mt-5"> + <SpinnerIcon className="animate-spin w-5 h-5 mr-2 mb-1 inline text-red" /> + Loading... + </div> )} {data && <MapathonSummaryResults data={data} />} </div> @@ -80,7 +84,10 @@ export const MapathonDetailedReport = () => { loading={isLoading || triggeredLoading} /> {(isLoading || triggeredLoading) && ( - <div className="mx-auto text-center w-1/4 p-1 mt-5">Loading...</div> + <div className="mx-auto text-center w-1/4 p-1 mt-5"> + <SpinnerIcon className="animate-spin w-5 h-5 mr-2 mb-1 inline text-red" /> + Loading... + </div> )} {data && ( <MapathonDetailedResultsTable diff --git a/src/views/UserGroupReport.js b/src/views/UserGroupReport.js index dd97de6..b6f8360 100644 --- a/src/views/UserGroupReport.js +++ b/src/views/UserGroupReport.js @@ -9,6 +9,7 @@ import { getUserIds, getUserStats } from "../queries/getUserStats"; import { MiniNavBar } from "../components/nav/navbar"; import { UserGroupColumnHeadings } from "../components/userGroup/constants"; import { aggregateUserGroupData } from "../utils/userGroupUtils"; +import { SpinnerIcon } from "../assets/svgIcons"; const userGroupPage = [ { pageTitle: "User Group Report", pageURL: "/user-report" }, @@ -18,18 +19,19 @@ export const UserGroupReport = () => { const { userGroupFormData, setUserGroupFormData } = useContext(FormContext); const [formError, setFormError] = useState(null); const [userIds, setUserIds] = useState([]); - const [loading, setLoading] = useState(false); const [users, setUsers] = useState(); - const { mutate, data, isLoading } = useMutation(getUserIds, { - onError: (error) => { + const { mutate, data, isLoading, error } = useMutation(getUserIds); + + useEffect(() => { + if (error) { if (error.response.status === 500) { setFormError(error.response.data); } else { setFormError(error.response.data.detail[0]["msg"]); } - }, - }); + } + }, [error]); useEffect(() => { if (data) setUserIds(data); @@ -44,7 +46,6 @@ export const UserGroupReport = () => { ...oldUsersArray, { ...i, stats: res }, ]); - setLoading(true); }) .catch((error) => { if (error.response.status === 500) { @@ -53,9 +54,6 @@ export const UserGroupReport = () => { setFormError(error.response.data.detail[0]["msg"]); } }) - .finally(() => { - setLoading(false); - }) ); }, [userGroupFormData] @@ -78,14 +76,18 @@ export const UserGroupReport = () => { formError={formError} setFormError={setFormError} /> - {(isLoading || loading) && ( - <div className="mx-auto text-center w-1/4 p-1 mt-5">Loading...</div> + {isLoading && ( + <div className="mx-auto text-center w-1/4 p-1 mt-5"> + <SpinnerIcon className="animate-spin w-5 h-5 mr-2 mb-1 inline text-red" /> + Loading... + </div> )} {data && ( <UserGroupResultsTable columns={UserGroupColumnHeadings} data={aggregateUserGroupData(users)} userDataCheck={userIds && userIds.length > 0} + loading={userIds.length !== users.length} /> )} </div>