diff --git a/client/src/__tests__/utils/UserRole.test.js b/client/src/__tests__/utils/UserRole.test.js index 99326483..8e55b5f6 100644 --- a/client/src/__tests__/utils/UserRole.test.js +++ b/client/src/__tests__/utils/UserRole.test.js @@ -133,10 +133,10 @@ test("Allowed to delete Invitation", () => { role: {id: "2", applicationUsages: applicationUsagesForManageId("11")} }; const user = {userRoles: [mail, research]} - const invitation = {intendedAuthority: AUTHORITIES.GUEST, roles: [mail, research]}; + const invitation = {intended_authority: AUTHORITIES.GUEST, roles: [mail, research]}; expect(allowedToDeleteInvitation(user, invitation)).toBeTruthy(); - invitation.intendedAuthority = AUTHORITIES.INVITER; + invitation.intended_authority = AUTHORITIES.INVITER; expect(allowedToDeleteInvitation(user, invitation)).toBeFalsy(); }); diff --git a/client/src/api/index.js b/client/src/api/index.js index 0aab4ce5..d4d5d18c 100644 --- a/client/src/api/index.js +++ b/client/src/api/index.js @@ -147,6 +147,14 @@ export function allInvitations() { return fetchJson(`/api/v1/invitations/all`, {}, {}, false); } +export function searchInvitations(roleId, pagination = {}) { + if (roleId) { + pagination.roleId = roleId; + } + const queryPart = paginationQueryParams(pagination, {}) + return fetchJson(`/api/v1/invitations/search?${queryPart}`, {}, {}, false); +} + //Manage export function allProviders() { return fetchJson("/api/v1/manage/providers"); diff --git a/client/src/components/Entities.jsx b/client/src/components/Entities.jsx index 1858c2b6..39836760 100644 --- a/client/src/components/Entities.jsx +++ b/client/src/components/Entities.jsx @@ -206,7 +206,7 @@ export const Entities = ({ } - {(!hasEntities && !initial && !customEmptySearch && !loading && !hideTitle) && + {(!hasEntities && !initial && !customEmptySearch && !loading && !hideTitle && !busy) &&

{customNoEntities || I18n.t(`${modelName}.noEntities`)}

} { diff --git a/client/src/locale/en.js b/client/src/locale/en.js index cee68ea7..f39f7765 100644 --- a/client/src/locale/en.js +++ b/client/src/locale/en.js @@ -227,10 +227,7 @@ const en = { delete: "Remove" }, invitations: { - found: "{{count}} {{plural}} found", - foundWithStatus: "{{count}} {{status}} {{plural}}", - singleInvitation: "invitation", - multipleInvitations: "invitations", + title: "Invitations", searchPlaceHolder: "Search for invitation...", noResults: "No invitation where found", inviter: "Invited by", @@ -254,7 +251,7 @@ const en = { roles: "Roles", inviterRoles: "Select the roles for the new invitation", rolesPlaceHolder: "Choose one or more roles", - expiryDate: "Invite valid till", + expiryDate: "Valid till", acceptedAt: "Date accepted", roleExpiryDate: "Role expiry date", roleExpiryDateQuestion: "Set a custom role expiration period", diff --git a/client/src/locale/nl.js b/client/src/locale/nl.js index 5e0d1dff..670b9cb3 100644 --- a/client/src/locale/nl.js +++ b/client/src/locale/nl.js @@ -227,10 +227,7 @@ const nl = { delete: "Verwijder" }, invitations: { - found: "{{count}} {{plural}} gevonden", - foundWithStatus: "{{count}} {{status}} {{plural}}", - singleInvitation: "uitnodiging", - multipleInvitations: "uitnodigingen", + title: "Uitnodigingen", searchPlaceHolder: "Zoek uitnodiging...", noResults: "Geen uitnodigingen gevonden", inviter: "Uitgenodigd door", @@ -254,7 +251,7 @@ const nl = { roles: "Rollen", inviterRoles: "Selecteer de rollen voor de nieuwe uitnodiging", rolesPlaceHolder: "Kies een of meer rollen", - expiryDate: "Uitnodiging geldig tot", + expiryDate: "Geldig tot", acceptedAt: "Datum geaccepteeerd", roleExpiryDate: "Verloopdatum rol", roleExpiryDateQuestion: "Zet een specifieke verloopdatum voor de rol", diff --git a/client/src/pages/Application.js b/client/src/pages/Application.js index d706f966..58519fc4 100644 --- a/client/src/pages/Application.js +++ b/client/src/pages/Application.js @@ -3,7 +3,7 @@ import {rolesPerApplicationManageId} from "../api"; import I18n from "../locale/I18n"; import "./Application.scss"; import {ReactComponent as WebsiteIcon} from "../icons/network-information.svg"; -import {Chip, Loader} from "@surfnet/sds"; +import {Chip} from "@surfnet/sds"; import {useNavigate, useParams} from "react-router-dom"; import {useAppStore} from "../stores/AppStore"; import {UnitHeader} from "../components/UnitHeader"; @@ -18,7 +18,7 @@ export const Application = () => { const {manageId} = useParams(); const navigate = useNavigate(); const {user} = useAppStore(state => state); - const [roles, setRoles] = useState({}); + const [roles, setRoles] = useState([]); const [application, setApplication] = useState({}); const [loading, setLoading] = useState(true); @@ -40,7 +40,6 @@ export const Application = () => { app.name = I18n.locale === "en" ? app["name:en"] || app["name:nl"] : app["name:nl"] || app["name:en"]; app.description = I18n.locale === "en" ? app["OrganizationName:en"] || app["OrganizationName:nl"] : app["OrganizationName:nl"] || app["OrganizationName:en"]; setApplication(app); - setLoading(false); const paths = [ {path: "/home", value: I18n.t("tabs.home")}, {path: "/home/applications", value: I18n.t("tabs.applications")}, @@ -49,17 +48,13 @@ export const Application = () => { useAppStore.setState({ breadcrumbPath: paths }); + // setTimeout(() => setLoading(false), 40); setLoading(false); }) .catch(() => navigate("/")) }, [user]); // eslint-disable-line react-hooks/exhaustive-deps - - if (loading) { - return - } - const openRole = (e, role) => { const path = `/roles/${role.id}` if (e.metaKey || e.ctrlKey) { @@ -128,6 +123,8 @@ export const Application = () => { modelName="applicationRoles" title={I18n.t("applications.title", {nbr: roles.length})} showNew={true} + busy={loading} + loading={false} newLabel={I18n.t("applications.new")} newEntityFunc={() => navigate("/role/new", {state: application.id})} defaultSort="name" diff --git a/client/src/pages/System.js b/client/src/pages/System.js index 200b3022..076ce462 100644 --- a/client/src/pages/System.js +++ b/client/src/pages/System.js @@ -40,7 +40,7 @@ export const System = () => { name="invitations" label={I18n.t("tabs.invitations")} > - + , { results.forEach(user => user.roleSummaries .sort((r1, r2) => (r1.endDate || Number.MAX_VALUE) - (r2.endDate || Number.MAX_VALUE))); setUsers(results); - //we need to avoid flickerings - setTimeout(() => setSearching(false), 75); - setTotalElements(page.totalElements); setSearching(false); + setTotalElements(page.totalElements); }); }, [paginationQueryParams]); diff --git a/client/src/tabs/Applications.js b/client/src/tabs/Applications.js index 3ad78414..9a02df76 100644 --- a/client/src/tabs/Applications.js +++ b/client/src/tabs/Applications.js @@ -29,8 +29,7 @@ const Applications = () => { const mergedApps = mergeProvidersProvisioningsRoles( res[0].providers, res[0].provisionings, res[1].content); setApplications(mergedApps); - //we need to avoid flickerings - setTimeout(() => setSearching(false), 75); + setSearching(false); }) }, []) // eslint-disable-line react-hooks/exhaustive-deps diff --git a/client/src/tabs/Invitations.js b/client/src/tabs/Invitations.js index de0ca407..66f63452 100644 --- a/client/src/tabs/Invitations.js +++ b/client/src/tabs/Invitations.js @@ -1,102 +1,94 @@ -import React, {useEffect, useRef, useState} from "react"; +import React, {useEffect, useState} from "react"; import I18n from "../locale/I18n"; import "./Invitations.scss"; -import {Button, ButtonSize, ButtonType, Checkbox, Chip, Loader, Tooltip} from "@surfnet/sds"; +import {Button, ButtonSize, ButtonType, Checkbox, Chip, Tooltip} from "@surfnet/sds"; import {Entities} from "../components/Entities"; import "./Users.scss"; import {shortDateFromEpoch} from "../utils/Date"; import {chipTypeForUserRole, invitationExpiry} from "../utils/Authority"; import {useNavigate} from "react-router-dom"; -import {allInvitations, deleteInvitation, invitationsByRoleId, resendInvitation} from "../api"; +import {deleteInvitation, resendInvitation, searchInvitations} from "../api"; import ConfirmationDialog from "../components/ConfirmationDialog"; import {useAppStore} from "../stores/AppStore"; import {isEmpty, pseudoGuid} from "../utils/Utils"; import {allowedToDeleteInvitation, AUTHORITIES, INVITATION_STATUS, isUserAllowed} from "../utils/UserRole"; -import {UnitHeader} from "../components/UnitHeader"; -import Select from "react-select"; import {ReactComponent as TrashIcon} from "@surfnet/sds/icons/functional-icons/bin.svg"; import {ReactComponent as ResendIcon} from "@surfnet/sds/icons/functional-icons/go-to-other-website.svg"; +import {defaultPagination, pageCount} from "../utils/Pagination"; +import debounce from "lodash.debounce"; -const allValue = "all"; -const mineValue = "mine"; export const Invitations = ({ role, - standAlone = false, - systemView = false, - history = false, - pending = true + systemView = false }) => { const navigate = useNavigate(); const {user, setFlash} = useAppStore(state => state); - const invitations = useRef(); + const [invitations, setInvitations] = useState([]); const [selectedInvitations, setSelectedInvitations] = useState({}); const [allSelected, setAllSelected] = useState(false); - const [resultAfterSearch, setResultAfterSearch] = useState([]) - const [loading, setLoading] = useState(true); + const [paginationQueryParams, setPaginationQueryParams] = useState(defaultPagination("email")); + const [totalElements, setTotalElements] = useState(0); + const [searching, setSearching] = useState(true); const [confirmation, setConfirmation] = useState({}); const [confirmationOpen, setConfirmationOpen] = useState(false); - const [filterOptions, setFilterOptions] = useState([]); - const [filterValue, setFilterValue] = useState(null); useEffect(() => { - const promise = systemView ? allInvitations() : invitationsByRoleId(role.id); - if (history) { - useAppStore.setState({ - breadcrumbPath: [ - {path: "/inviter", value: I18n.t("tabs.home")}, - {value: I18n.t("tabs.invitations")} - ] - }); - } - promise.then(res => { - res.forEach(invitation => { - invitation.intendedRoles = invitation.roles - .sort((r1, r2) => r1.role.name.localeCompare(r2.role.name)) - .map(role => role.role.name).join(", "); - const now = new Date(); - invitation.status = new Date(invitation.expiryDate * 1000) < now ? INVITATION_STATUS.EXPIRED : invitation.status; - }); - setSelectedInvitations(res - .reduce((acc, invitation) => { - acc[invitation.id] = { - selected: false, - ref: invitation, - allowed: allowedToDeleteInvitation(user, invitation) - }; - return acc; - }, {})); - invitations.current = res; - const newFilterOptions = [{ - label: I18n.t("invitations.statuses.all", {nbr: res.length}), - value: allValue - }]; - const statusOptions = res.reduce((acc, invitation) => { - const option = acc.find(opt => opt.status === invitation.status); - if (option) { - ++option.nbr; - } else { - acc.push({status: invitation.status, nbr: 1}) - } - return acc; - }, []).map(option => ({ - label: `${I18n.t("invitations.statuses." + option.status.toLowerCase())} (${option.nbr})`, - value: option.status - })).concat({ - label: `${I18n.t("invitations.statuses.mine")} (${res.filter(inv => inv.inviter.email === user.email).length})`, - value: mineValue - }).sort((o1, o2) => o1.label.localeCompare(o2.label)); - - setFilterOptions(newFilterOptions.concat(statusOptions)); - setFilterValue(newFilterOptions[0]); + searchInvitations(systemView ? null : role.id, paginationQueryParams) + .then(page => { + const content = page.content; + content.forEach(invitation => { + invitation.intendedRoles = (invitation.roles || []) + .sort((r1, r2) => r1.name.localeCompare(r2.name)) + .map(role => role.name).join(", "); + const now = new Date(); + invitation.status = new Date(invitation.expiryDate * 1000) < now ? INVITATION_STATUS.EXPIRED : invitation.status; + //We don't get the invitation.user_roles.role.applicationUsages from the server anymore due to custom pagination queries + (invitation.roles || []).forEach(invitationRole => { + invitationRole.role = { + id: invitationRole.id, + applicationUsages: invitationRole.manageIdentifiers.map(mi => ({application: {manageId: mi}})), + user_id: invitationRole.user_id, + authority: invitationRole.intended_authority + } + }); - setResultAfterSearch(res); - //we need to avoid flickerings - setTimeout(() => setLoading(false), 75); - }) + }); + setInvitations(content); + setSelectedInvitations(content + .reduce((acc, invitation) => { + acc[invitation.id] = { + selected: false, + ref: invitation, + allowed: allowedToDeleteInvitation(user, invitation) + }; + return acc; + }, {})); + setAllSelected(false); + setTotalElements(page.totalElements); + setSearching(false); + }) }, - [invitations, user]) // eslint-disable-line react-hooks/exhaustive-deps + [user, paginationQueryParams]) // eslint-disable-line react-hooks/exhaustive-deps + + const search = (query, sorted, reverse, page) => { + if (isEmpty(query) || query.trim().length > 2) { + delayedAutocomplete(query, sorted, reverse, page); + } + }; + + const delayedAutocomplete = debounce((query, sorted, reverse, page) => { + setSearching(true); + //this will trigger a new search + setPaginationQueryParams({ + query: query, + pageNumber: page, + pageSize: pageCount, + sort: sorted, + sortDirection: reverse ? "DESC" : "ASC" + }) + }, 375); const onCheck = invitation => e => { const checked = e.target.checked; @@ -119,8 +111,8 @@ export const Invitations = ({ const invitationIdentifiers = () => { return Object.entries(selectedInvitations) .filter(entry => (entry[1].selected) && entry[1].allowed) - .map(entry => parseInt(entry[0])) - .filter(id => resultAfterSearch.some(res => res.id === id)); + .map(entry => parseInt(entry[0])); + } const showCheckAllHeader = () => { @@ -144,8 +136,7 @@ export const Invitations = ({ .then(() => { setConfirmationOpen(false); setFlash(I18n.t("invitations.resendFlash")); - const path = encodeURIComponent(window.location.pathname); - navigate(`/refresh-route/${path}`, {replace: true}); + setPaginationQueryParams({...paginationQueryParams}); }) } }; @@ -183,8 +174,7 @@ export const Invitations = ({ .then(() => { setConfirmationOpen(false); setFlash(I18n.t("invitations.deleteFlash")); - const path = encodeURIComponent(window.location.pathname); - navigate(`/refresh-route/${path}`, {replace: true}); + setPaginationQueryParams({...paginationQueryParams}); }) } }; @@ -203,20 +193,11 @@ export const Invitations = ({ .then(() => { setConfirmationOpen(false); setFlash(I18n.t("invitations.deleteFlash")); - const path = encodeURIComponent(window.location.pathname); - navigate(`/refresh-route/${path}`, {replace: true}); + setPaginationQueryParams({...paginationQueryParams}); }) } }; - if (loading) { - return - } - - const searchCallback = afterSearch => { - setResultAfterSearch(afterSearch); - } - const actionButtons = () => { if (isEmpty(invitationIdentifiers())) { return null; @@ -234,35 +215,19 @@ export const Invitations = ({ txt={I18n.t("invitations.delete")}/> }/> - {pending && -
- doResendInvitations(true)} - size={ButtonSize.Small} - type={ButtonType.Secondary} - txt={I18n.t("invitations.resend")}/> - }/> -
} - ); - } - const filter = () => { - return ( -
-