Skip to content

Commit cf5bf7a

Browse files
committed
Fixes #786
1 parent 1925c3a commit cf5bf7a

14 files changed

Lines changed: 596 additions & 17 deletions

File tree

client/src/api/index.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,21 @@ export function institutionAdminsbyRole(roleId) {
9999
return fetchJson(`/api/v1/users/institution-admins/${roleId}`, {}, {}, false);
100100
}
101101

102-
export function institutionAdmins() {
103-
return fetchJson("/api/v1/users/institutionAdmins", {}, {}, false);
102+
export function applicationManagers() {
103+
return fetchJson("/api/v1/users/applicationManagers", {}, {}, false);
104+
}
105+
106+
export function institutionAdmins(includeMe=false) {
107+
const queryPart = includeMe ? "?includeMe=true" : ""
108+
return fetchJson(`/api/v1/users/institutionAdmins${queryPart}`, {}, {}, false);
109+
}
110+
111+
export function removeInstitutionAdmin(user) {
112+
return postPutJson(`/api/v1/users/removeInstitutionAdmin/${user.id}`, {}, "PUT", false);
113+
}
114+
115+
export function removeApplicationManager(user) {
116+
return postPutJson(`/api/v1/users/removeApplicationManager/${user.id}`, {}, "PUT", false);
104117
}
105118

106119
export function searchUsers(pagination = {}) {

client/src/components/UserMenu.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export const UserMenu = ({user, actions}) => {
3333

3434
const renderMenu = adminLinks => {
3535
const authority = highestAuthority(user);
36-
const apiTokenLink = authority === AUTHORITIES.INVITER || authority === AUTHORITIES.MANAGER || authority === AUTHORITIES.APPLICATION_MANAGER;
36+
const apiTokenLink = authority === AUTHORITIES.INVITER || authority === AUTHORITIES.MANAGER ||
37+
authority === AUTHORITIES.APPLICATION_MANAGER || authority === AUTHORITIES.INSTITUTION_ADMIN;
3738
return (<>
3839
<ul>
3940
{user.superUser && adminLinks.map(l =>
@@ -43,6 +44,9 @@ export const UserMenu = ({user, actions}) => {
4344
{apiTokenLink && <li>
4445
<Link onClick={toggleUserMenu} to={`/tokens`}>{I18n.t(`header.links.tokens`)}</Link>
4546
</li>}
47+
{(!user.superUser && user.institutionAdmin && user.organizationGUID) && <li>
48+
<Link onClick={toggleUserMenu} to={`/institution-admins`}>{I18n.t(`header.links.colleagues`)}</Link>
49+
</li>}
4650
{(user.superUser || (user.institutionAdmin && user.organizationGUID)) && <li>
4751
<Link onClick={toggleUserMenu} to={`/audit`}>{I18n.t(`header.links.audit`)}</Link>
4852
</li>}

client/src/locale/en.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ const en = {
4040
profile: "Profile",
4141
tokens: "Tokens",
4242
logout: "Log out",
43-
audit: "Role history"
43+
audit: "Role history",
44+
colleagues: "Institution roles"
4445
},
4546
},
4647
tabs: {
@@ -135,6 +136,8 @@ const en = {
135136
deleteConfirmation: "Are you absolutely sure you want to delete user {{name}}? There is no undo button.",
136137
institutionAdmins: "Your fellow Institution administrators are:",
137138
onlyInstitutionAdmins: "You are the only Institution admin within your organisation",
139+
removeInstitutionAdminConfirmation: "Are you sure you want to remove the institution admin rights from {{userName}}",
140+
removeInstitutionAdminFlash: "Institution rights are removed from {{userName}}"
138141
},
139142
role: {
140143
copyUrn: "Copy urn",
@@ -591,6 +594,10 @@ const en = {
591594
searchPlaceHolder: "Search for role history",
592595
title: "The audittrail of Roles",
593596
rolePlaceHolder: "Filter by role",
597+
},
598+
institutionAdmins: {
599+
title: "Institution administrators and application managers",
600+
searchPlaceHolder: "Search..."
594601
}
595602
}
596603

client/src/locale/nl.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ const nl = {
4040
profile: "Profiel",
4141
tokens: "Tokens",
4242
logout: "Uitloggen",
43-
audit: "Rol historie"
43+
audit: "Rol historie",
44+
colleagues: "Instellingsrollen"
4445
},
4546
},
4647
tabs: {
@@ -134,7 +135,10 @@ const nl = {
134135
deleteFlash: "Gebruiker {{name}} is verwijderd",
135136
deleteConfirmation: "Weet je heel zeker dat je gebruiker {{name}} wilt verwijderen? Er is geen undo functionaliteit.",
136137
institutionAdmins: "Je collega-instellingsbeheerders zijn:",
137-
onlyInstitutionAdmins: "Je bent de enige instellingsbeheerder binnen je organisatie", },
138+
onlyInstitutionAdmins: "Je bent de enige instellingsbeheerder binnen je organisatie",
139+
removeInstitutionAdminConfirmation: "Weet je zeker dat je de beheerdersrechten voor de instelling wilt intrekken bij {{userName}}?",
140+
removeInstitutionAdminFlash: "De beheerdersrechten voor de instelling zijn ingetrokken bij {{userName}}",
141+
},
138142
role: {
139143
copyUrn: "Copy urn",
140144
userInfo: "{{nbr}} leden & verloopt {{period}}",
@@ -591,8 +595,11 @@ const nl = {
591595
searchPlaceHolder: "Zoeken naar rolgeschiedenis",
592596
title: "Het auditspoor van rollen",
593597
rolePlaceHolder: "Filteren op rol",
598+
},
599+
institutionAdmins: {
600+
title: "Beheerders van de instelling",
601+
searchPlaceHolder: "Zoeken..."
594602
}
595-
596603
}
597604

598605
export default nl;

client/src/pages/App.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {flushSync} from "react-dom";
2929
import {UserTokens} from "./UserTokens";
3030
import {Busy} from "./Busy";
3131
import {UserRoleAudits} from "../tabs/UserRoleAudits";
32+
import {InstitutionAdmins} from "../tabs/InstitutionAdmins";
3233

3334

3435
export const App = () => {
@@ -121,6 +122,7 @@ export const App = () => {
121122
<Route path="invitation/accept"
122123
element={<Invitation authenticated={true}/>}/>
123124
<Route path="login" element={<Login/>}/>
125+
<Route path="institution-admins" element={<InstitutionAdmins/>}/>
124126
<Route path="refresh-route/:path" element={<RefreshRoute/>}/>
125127
{(user && user.superUser) &&
126128
<Route path="system/:tab?" element={<System/>}/>

client/src/pages/Home.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export const Home = () => {
6161
label={I18n.t("tabs.invitations")}>
6262
<MineInvitations/>
6363
</Page> : null,
64-
(user && (user.superUser || (user.institutionAdmin && user.organizationGUID))) ?
64+
(user && user.superUser) ?
6565
<Page key="tokens"
6666
name="tokens"
6767
label={I18n.t("tabs.tokens")}>

client/src/pages/InvitationForm.jsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ export const InvitationForm = () => {
248248
const res = required.every(attr => !isEmpty(invitation[attr])) &&
249249
(!isEmpty(selectedRoles) || invitationIsForAdmin || invitation.intendedAuthority === AUTHORITIES.APPLICATION_MANAGER) &&
250250
!(invitation.intendedAuthority === AUTHORITIES.INSTITUTION_ADMIN && isEmpty(invitation.organizationGUID)) &&
251-
(!isEmpty(selectedApplications) || invitation.intendedAuthority !== AUTHORITIES.INSTITUTION_ADMIN);
251+
(!isEmpty(selectedApplications) || invitation.intendedAuthority !== AUTHORITIES.APPLICATION_MANAGER);
252252
return res;
253253
}
254254

@@ -343,20 +343,23 @@ export const InvitationForm = () => {
343343

344344

345345
const authorityChanged = option => {
346+
const applicationManagerAuthority = option.value === AUTHORITIES.APPLICATION_MANAGER;
347+
const adminAuthority = option.value === AUTHORITIES.SUPER_USER || option.value === AUTHORITIES.INSTITUTION_ADMIN;
346348
setInvitation({
347349
...invitation,
348350
intendedAuthority: option.value,
351+
eduIDOnly: applicationManagerAuthority || adminAuthority ? false : invitation.eduIDOnly,
349352
roleExpiryDate: defaultRoleExpiryDate(selectedRoles),
350353
organizationGUID: option.value !== AUTHORITIES.INSTITUTION_ADMIN ? null :
351354
(user.institutionAdmin ? user.organizationGUID : null)
352355
});
353-
if (option.value === AUTHORITIES.SUPER_USER || option.value === AUTHORITIES.INSTITUTION_ADMIN) {
356+
if (adminAuthority) {
354357
setSelectedRoles([]);
355358
}
356359
if (option.value !== AUTHORITIES.INSTITUTION_ADMIN) {
357360
setOrganizationGUIDIdentityProvider({});
358361
}
359-
if (option.value === AUTHORITIES.APPLICATION_MANAGER) {
362+
if (applicationManagerAuthority) {
360363
setSelectedRoles([]);
361364
setOrganizationGUIDIdentityProvider({});
362365
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import React, {useCallback, useEffect, useState} from "react";
2+
import I18n from "../locale/I18n";
3+
import "../components/Entities.scss";
4+
import {Chip, ChipType, Loader, Tooltip} from "@surfnet/sds";
5+
import {Entities} from "../components/Entities";
6+
import {applicationManagers, institutionAdmins, removeApplicationManager, removeInstitutionAdmin} from "../api";
7+
import UserIcon from "@surfnet/sds/icons/functional-icons/id-2.svg";
8+
import "./InstitutionAdmins.scss";
9+
import {useNavigate} from "react-router-dom";
10+
import {useAppStore} from "../stores/AppStore";
11+
import {dateFromEpoch, shortDateFromEpoch} from "../utils/Date";
12+
import {AUTHORITIES} from "../utils/UserRole";
13+
import {chipTypeForUserRole} from "../utils/Authority";
14+
import TrashIcon from "@surfnet/sds/icons/functional-icons/bin.svg";
15+
import ConfirmationDialog from "../components/ConfirmationDialog";
16+
import {isEmpty} from "../utils/Utils";
17+
18+
export const InstitutionAdmins = () => {
19+
20+
const {user: currentUser, setFlash} = useAppStore(state => state);
21+
22+
const [confirmation, setConfirmation] = useState({});
23+
const [confirmationOpen, setConfirmationOpen] = useState(false);
24+
const [admins, setAdmins] = useState([]);
25+
const [loading, setLoading] = useState(true);
26+
const navigate = useNavigate();
27+
28+
const loadAdmins = useCallback(() => {
29+
Promise.all([applicationManagers(), institutionAdmins(true)])
30+
.then(res => {
31+
setAdmins(res[0].concat(res[1]));
32+
})
33+
.finally(() => {
34+
setLoading(false);
35+
});
36+
}, []);
37+
38+
useEffect(() => {
39+
if (!currentUser.institutionAdmin || !currentUser.organizationGUID) {
40+
navigate("/home");
41+
return;
42+
}
43+
loadAdmins();
44+
}, [currentUser.institutionAdmin, currentUser.organizationGUID, loadAdmins, navigate]);
45+
46+
if (loading) {
47+
return <Loader/>
48+
}
49+
50+
const columns = [
51+
{
52+
nonSortable: true,
53+
key: "icon",
54+
header: "",
55+
mapper: user => <div className="member-icon">
56+
<Tooltip standalone={true}
57+
children={<UserIcon/>}
58+
tip={I18n.t("tooltips.userIcon",
59+
{
60+
name: user.name,
61+
createdAt: dateFromEpoch(user.createdAt, false),
62+
lastActivity: dateFromEpoch(user.lastActivity, false)
63+
})}/>
64+
</div>
65+
},
66+
{
67+
key: "name",
68+
header: I18n.t("users.name_email"),
69+
mapper: user => (
70+
<div className="user-name-email">
71+
<span className="name">{user.name}</span>
72+
<span className="email">{user.email}</span>
73+
</div>)
74+
},
75+
{
76+
key: "schac_home_organization",
77+
header: I18n.t("users.schacHomeOrganization"),
78+
mapper: user => <span>{user.schac_home_organization}</span>
79+
},
80+
{
81+
key: "authority",
82+
header: I18n.t("users.highestAuthority"),
83+
mapper: user => {
84+
const authority = user.institution_admin ? AUTHORITIES.INSTITUTION_ADMIN : AUTHORITIES.APPLICATION_MANAGER;
85+
return <Chip type={chipTypeForUserRole(authority)}
86+
label={I18n.t(`access.${authority}`)}/>
87+
}
88+
},
89+
{
90+
key: "createdAt",
91+
header: I18n.t("users.createdAt"),
92+
mapper: user => shortDateFromEpoch(user.createdAt, false)
93+
},
94+
{
95+
key: "lastActivity",
96+
header: I18n.t("users.lastActivity"),
97+
mapper: user => shortDateFromEpoch(user.lastActivity, false)
98+
},
99+
{
100+
key: "roles",
101+
header: I18n.t("users.roles"),
102+
mapper: user => isEmpty(user.roles) ? "-" :
103+
<ul>{user.roles.split(",").map(role => <li key={role}>{role}</li>)}</ul>
104+
},
105+
{
106+
nonSortable: true,
107+
key: "actions",
108+
header: "",
109+
mapper: user => (currentUser.id !== user.id) ?
110+
((user.institution_admin_by_invite || !isEmpty(user.roles)) ? <
111+
span onClick={() => doRemoveInstitutionAdmin(user, true)}>
112+
<TrashIcon/>
113+
</span> : null)
114+
: <Chip type={ChipType.Main_400} label={I18n.t("forms.you")}/>
115+
}
116+
].filter(column => column !== null);
117+
118+
const doRemoveInstitutionAdmin = (user, showConfirmation) => {
119+
const isInstitutionAdmin = user.institution_admin;
120+
if (showConfirmation) {
121+
setConfirmation({
122+
cancel: () => setConfirmationOpen(false),
123+
action: () => doRemoveInstitutionAdmin(user, false),
124+
question: I18n.t("users.removeInstitutionAdminConfirmation", {
125+
userName: user.name
126+
}),
127+
confirmationTxt: I18n.t("confirmationDialog.confirm"),
128+
confirmationHeader: I18n.t("confirmationDialog.title")
129+
});
130+
setConfirmationOpen(true);
131+
} else {
132+
setLoading(true);
133+
const promise = isInstitutionAdmin ? removeInstitutionAdmin(user) : removeApplicationManager(user);
134+
promise
135+
.then(() => {
136+
setConfirmationOpen(false);
137+
setFlash(I18n.t("users.removeInstitutionAdminFlash", {userName: user.name}));
138+
loadAdmins();
139+
}).finally(() => {
140+
setLoading(false);
141+
})
142+
}
143+
};
144+
145+
return (
146+
<div className="mod-users">
147+
{confirmationOpen && <ConfirmationDialog isOpen={confirmationOpen}
148+
cancel={confirmation.cancel}
149+
confirm={confirmation.action}
150+
confirmationTxt={confirmation.confirmationTxt}
151+
confirmationHeader={confirmation.confirmationHeader}
152+
isError={confirmation.error}
153+
question={confirmation.question}/>}
154+
<Entities entities={admins}
155+
modelName="institutionAdmins"
156+
defaultSort="name"
157+
columns={columns}
158+
newLabel={currentUser.superUser ? I18n.t("invitations.newInvite") : null}
159+
showNew={false}
160+
customNoEntities={I18n.t(`users.noResults`)}
161+
searchAttributes={["name", "email", "schacHomeOrganization"]}
162+
inputFocus={true}
163+
/>
164+
</div>
165+
);
166+
167+
}

0 commit comments

Comments
 (0)