Skip to content

Commit d83031d

Browse files
committed
Fixes #277
1 parent 858aebe commit d83031d

17 files changed

Lines changed: 661 additions & 9 deletions

File tree

client/src/api/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,12 @@ export function performanceSeed() {
280280
export function rolesUnknownInManage() {
281281
return fetchJson("/api/v1/system/unknown-roles")
282282
}
283+
//Audit
284+
export function searchUserRoleAudits(pagination = {}, roleId = null) {
285+
const queryPart = paginationQueryParams(pagination, {roleId: roleId});
286+
return fetchJson(`/api/v1/user_roles_audit/search?${queryPart}`);
287+
}
288+
289+
export function fetchRoles() {
290+
return fetchJson("/api/v1/user_roles_audit/roles");
291+
}

client/src/components/Entities.scss

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,15 @@
5555

5656
.search-filter {
5757
display: flex;
58-
width: 290px;
58+
width: 320px;
59+
margin-right: 40px;
5960
@media (max-width: vars.$medium) {
6061
flex-direction: column;
6162
margin: 15px 0;
6263
}
64+
.select-field {
65+
width: 100%;
66+
}
6367
}
6468

6569
div.filter-select__value-container {

client/src/components/SelectField.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ export default function SelectField({
1414
}) {
1515
return (
1616
<div className={`select-field ${className}`}>
17-
<label htmlFor={name}>{name}{required && <sup className="required">*</sup>}
17+
{name && <label htmlFor={name}>{name}{required && <sup className="required">*</sup>}
1818
{toolTip && <Tooltip tip={toolTip}/>}
19-
</label>
19+
</label>}
2020
{creatable &&
2121
<CreatableSelect
2222
className={`input-select-inner creatable`}

client/src/components/UserMenu.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export const UserMenu = ({user, actions}) => {
4343
{apiTokenLink && <li>
4444
<Link onClick={toggleUserMenu} to={`/tokens`}>{I18n.t(`header.links.tokens`)}</Link>
4545
</li>}
46+
{(user.superUser || (user.institutionAdmin && user.organizationGUID)) && <li>
47+
<Link onClick={toggleUserMenu} to={`/audit`}>{I18n.t(`header.links.audit`)}</Link>
48+
</li>}
4649
<li>
4750
<Link onClick={toggleUserMenu} to={`/profile`}>{I18n.t(`header.links.profile`)}</Link>
4851
</li>

client/src/locale/en.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ const en = {
3939
help: "Help",
4040
profile: "Profile",
4141
tokens: "Tokens",
42-
logout: "Log out"
42+
logout: "Log out",
43+
audit: "Role history"
4344
},
4445
},
4546
tabs: {
@@ -559,6 +560,24 @@ const en = {
559560
ValidateNamesExternal: "EduID validated name by an external (non institutional) source",
560561
EduIDRequireStudentAffiliation: "EduID required student affiliation",
561562
TransparentAuthnContext: "Application provides ACR value"
563+
},
564+
userRoleAudit: {
565+
header: "Audittrail",
566+
info: "All mutations to roles created by your organisation",
567+
email: "User",
568+
roleName: "Role",
569+
action: "Action",
570+
actions: {
571+
ADD: "Added",
572+
DELETE: "Deleted",
573+
UPDATE: "Updated"
574+
},
575+
authority: "Authority",
576+
endDate: "End date",
577+
createdAt: "Created",
578+
searchPlaceHolder: "Search for role history",
579+
title: "The audittrail of Roles",
580+
rolePlaceHolder: "Filter by role",
562581
}
563582
}
564583

client/src/locale/nl.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ const nl = {
3939
help: "Help",
4040
profile: "Profiel",
4141
tokens: "Tokens",
42-
logout: "Uitloggen"
42+
logout: "Uitloggen",
43+
audit: "Rol historie"
4344
},
4445
},
4546
tabs: {
@@ -560,6 +561,24 @@ const nl = {
560561
ValidateNamesExternal: "EduID gevalideerde naam bij een externe (niet institutioneel) bron",
561562
EduIDRequireStudentAffiliation: "EduID verplicht student affiliation",
562563
TransparentAuthnContext: "Application levert zelf ACR waarde"
564+
},
565+
userRoleAudit: {
566+
header: "Audittrail",
567+
info: "Alle wijzigingen aan rollen die door je organisatie zijn aangemaakt",
568+
email: "Gebruiker",
569+
roleName: "Rol",
570+
action: "Actie",
571+
actions: {
572+
ADD: "Toegevoegd",
573+
DELETE: "Verwijderd",
574+
UPDATE: "Bijgewerkt"
575+
},
576+
authority: "Autoriteit",
577+
endDate: "Einddatum",
578+
createdAt: "Aangemaakt",
579+
searchPlaceHolder: "Zoeken naar rolgeschiedenis",
580+
title: "Het auditspoor van rollen",
581+
rolePlaceHolder: "Filteren op rol",
563582
}
564583

565584
}

client/src/pages/App.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {System} from "./System";
2828
import {flushSync} from "react-dom";
2929
import {UserTokens} from "./UserTokens";
3030
import {Busy} from "./Busy";
31+
import {UserRoleAudits} from "../tabs/UserRoleAudits";
3132

3233

3334
export const App = () => {
@@ -116,6 +117,7 @@ export const App = () => {
116117
<Route path="roles/:id/:tab?" element={<Role/>}/>
117118
<Route path="applications/:manageId" element={<Application/>}/>
118119
<Route path="tokens" element={<UserTokens/>}/>
120+
<Route path="audit" element={<UserRoleAudits/>}/>
119121
<Route path="invitation/accept"
120122
element={<Invitation authenticated={true}/>}/>
121123
<Route path="login" element={<Login/>}/>

client/src/pages/Home.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export const Home = () => {
6565
if (highestAuthority(user) === AUTHORITIES.INVITER) {
6666
navigate("/inviter");
6767
}
68-
68+
tabChanged(currentTab);
6969
}, [user, navigate]);
7070

7171
const tabChanged = (name) => {
@@ -74,7 +74,7 @@ export const Home = () => {
7474
useAppStore.setState({
7575
breadcrumbPath: [
7676
{path: "/home", value: I18n.t("tabs.home")},
77-
{value: I18n.t(`tabs.${currentTab}`)}
77+
{value: I18n.t(`tabs.${name}`)}
7878
]
7979
});
8080
}

client/src/tabs/UserRoleAudits.jsx

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import "./UserRoleAudits.scss";
2+
import {useAppStore} from "../stores/AppStore";
3+
import React, {useEffect, useState} from "react";
4+
import {Entities} from "../components/Entities";
5+
import I18n from "../locale/I18n";
6+
import {useNavigate} from "react-router-dom";
7+
import {AUTHORITIES, isUserAllowed} from "../utils/UserRole";
8+
import {fetchRoles, searchUserRoleAudits} from "../api";
9+
import {isEmpty} from "../utils/Utils";
10+
import debounce from "lodash.debounce";
11+
import {defaultPagination, pageCount} from "../utils/Pagination";
12+
import {dateFromEpoch, shortDateFromEpoch} from "../utils/Date";
13+
import SelectField from "../components/SelectField";
14+
import {UnitHeader} from "../components/UnitHeader";
15+
import Logo from "@surfnet/sds/icons/illustrative-icons/database-hand.svg";
16+
17+
export const UserRoleAudits = () => {
18+
const {user} = useAppStore(state => state);
19+
20+
const [searching, setSearching] = useState(isUserAllowed(AUTHORITIES.INSTITUTION_ADMIN, user));
21+
const [userRoleAudits, setUserRoleAudits] = useState([]);
22+
const [roles, setRoles] = useState([]);
23+
const [selectedRole, setSelectedRole] = useState(null);
24+
const [paginationQueryParams, setPaginationQueryParams] = useState(defaultPagination("userEmail"));
25+
const [totalElements, setTotalElements] = useState(0);
26+
const navigate = useNavigate();
27+
28+
if (!isUserAllowed(AUTHORITIES.INSTITUTION_ADMIN, user)) {
29+
navigate("/home")
30+
}
31+
32+
useEffect(() => {
33+
fetchRoles()
34+
.then(res => {
35+
setRoles(res);
36+
useAppStore.setState({
37+
breadcrumbPath: [
38+
{path: "/home", value: I18n.t("tabs.home")},
39+
{value: I18n.t("header.links.audit")}
40+
]
41+
});
42+
})
43+
}, [user]);
44+
45+
useEffect(() => {
46+
searchUserRoleAudits(paginationQueryParams, isEmpty(selectedRole) ? null : selectedRole.value)
47+
.then(page => {
48+
setUserRoleAudits(page.content);
49+
setTotalElements(page.totalElements);
50+
setSearching(false);
51+
})
52+
}, [paginationQueryParams, selectedRole]);
53+
54+
const search = (query, sorted, reverse, page) => {
55+
const paginationQueryParamsChanged = sorted !== paginationQueryParams.sort || reverse !== paginationQueryParams.sortDirection ||
56+
page !== paginationQueryParams.pageNumber;
57+
if ((!isEmpty(query) && query.trim().length > 2) || paginationQueryParamsChanged) {
58+
delayedAutocomplete(query, sorted, reverse, page);
59+
}
60+
};
61+
62+
const delayedAutocomplete = debounce((query, sorted, reverse, page) => {
63+
setSearching(true);
64+
//this will trigger a new search
65+
setPaginationQueryParams({
66+
query: query,
67+
pageNumber: page,
68+
pageSize: pageCount,
69+
sort: sorted,
70+
sortDirection: reverse ? "DESC" : "ASC"
71+
})
72+
}, 375);
73+
74+
const filters = () => {
75+
return (
76+
<SelectField
77+
value={selectedRole}
78+
options={roles.map(role => ({value:role.id, label: role.name}))}
79+
searchable={true}
80+
clearable={true}
81+
placeholder={I18n.t("userRoleAudit.rolePlaceHolder")}
82+
onChange={option => setSelectedRole(option)}
83+
/>
84+
)
85+
}
86+
87+
const columns = [
88+
{
89+
key: "userEmail",
90+
header: I18n.t("userRoleAudit.email"),
91+
mapper: userRoleAudit => userRoleAudit.userEmail
92+
},
93+
{
94+
key: "roleName",
95+
header: I18n.t("userRoleAudit.roleName"),
96+
mapper: userRoleAudit => userRoleAudit.roleName
97+
},
98+
{
99+
key: "action",
100+
header: I18n.t("userRoleAudit.action"),
101+
mapper: userRoleAudit => I18n.t(`userRoleAudit.actions.${userRoleAudit.action}`)
102+
},
103+
{
104+
key: "authority",
105+
header: I18n.t("userRoleAudit.authority"),
106+
mapper: userRoleAudit => userRoleAudit.authority
107+
},
108+
{
109+
key: "createdAt",
110+
header: I18n.t("userRoleAudit.createdAt"),
111+
mapper: userRoleAudit => shortDateFromEpoch(userRoleAudit.createdAt, true)
112+
},
113+
{
114+
key: "endDate",
115+
header: I18n.t("userRoleAudit.endDate"),
116+
mapper: userRoleAudit => shortDateFromEpoch(userRoleAudit.endDate, true)
117+
},
118+
];
119+
120+
return (
121+
<div className={"mod-user-role-audits"}>
122+
<UnitHeader obj={({name: I18n.t("userRoleAudit.header"), svg: Logo, style: "small"})}>
123+
<p>{I18n.t("userRoleAudit.info")}</p>
124+
</UnitHeader>
125+
<Entities
126+
entities={userRoleAudits}
127+
modelName="userRoleAudit"
128+
showNew={false}
129+
defaultSort="userEmail"
130+
columns={columns}
131+
searchAttributes={["userEmail", "roleName"]}
132+
loading={false}
133+
filters={filters()}
134+
inputFocus={!searching}
135+
hideTitle={searching}
136+
customSearch={search}
137+
totalElements={totalElements}
138+
busy={searching}
139+
/>
140+
</div>
141+
);
142+
143+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
.mod-user-role-audits {
2+
3+
table.userRoleAudit {
4+
5+
thead {
6+
th {
7+
&.userEmail {
8+
width: 20%;
9+
}
10+
11+
&.roleName {
12+
width: 30%;
13+
}
14+
15+
&.action {
16+
width: 12%;
17+
}
18+
19+
&.authority {
20+
width: 12%;
21+
}
22+
23+
&.createdAt {
24+
width: 12%;
25+
}
26+
27+
&.endDate {
28+
width: 12%;
29+
}
30+
31+
}
32+
}
33+
34+
}
35+
36+
}

0 commit comments

Comments
 (0)