From 0026f9daa39a556ce9227bd5f5c48acffd0f5855 Mon Sep 17 00:00:00 2001 From: Okke Harsta Date: Thu, 21 Sep 2023 17:43:00 +0200 Subject: [PATCH] WIp for new design --- client/src/components/Entities.jsx | 11 ++- client/src/components/Entities.scss | 8 +- client/src/components/UnitHeader.jsx | 4 +- client/src/components/UnitHeader.scss | 1 + client/src/locale/en.js | 10 ++- client/src/pages/Role.js | 6 +- client/src/tabs/Invitations.js | 2 +- client/src/tabs/Roles.js | 86 ++++++++----------- client/src/tabs/Roles.scss | 11 +-- client/src/tabs/UserRoles.js | 14 +-- client/src/tabs/UserRoles.scss | 8 ++ client/src/utils/Manage.js | 11 +++ client/src/utils/UserRole.js | 18 ++-- .../main/java/access/api/RoleController.java | 6 +- .../java/access/api/UserRoleController.java | 2 +- .../src/main/java/access/manage/Manage.java | 24 +++++- server/src/main/java/access/model/Role.java | 3 + .../java/access/model/UpdateUserRole.java | 1 - welcome/src/components/RoleCard.scss | 19 +++- welcome/src/components/User.js | 4 +- welcome/src/pages/App.js | 2 +- 21 files changed, 156 insertions(+), 95 deletions(-) diff --git a/client/src/components/Entities.jsx b/client/src/components/Entities.jsx index 7517b082..2f559867 100644 --- a/client/src/components/Entities.jsx +++ b/client/src/components/Entities.jsx @@ -82,11 +82,6 @@ export const Entities = ({ const filterClassName = !hideTitle && filters ? "filters-with-title" : `${modelName}-search-filters`; return (
- {showNew && -
); }; diff --git a/client/src/components/Entities.scss b/client/src/components/Entities.scss index af7eb389..d587f930 100644 --- a/client/src/components/Entities.scss +++ b/client/src/components/Entities.scss @@ -20,14 +20,19 @@ align-items: center; padding: 32px 0 28px 0; min-height: 40px; - justify-content: space-between; + justify-content: flex-end; @media (max-width: $medium) { flex-direction: column; align-items: normal; } + button { + margin-left: 25px; + } + h2 { + margin-right: auto; @media (max-width: $medium) { margin-bottom: 15px } @@ -139,6 +144,7 @@ &.logo { width: 85px; + height: auto; } svg.fa-caret-up, svg.fa-caret-down { diff --git a/client/src/components/UnitHeader.jsx b/client/src/components/UnitHeader.jsx index 5376fdab..97f7e20b 100644 --- a/client/src/components/UnitHeader.jsx +++ b/client/src/components/UnitHeader.jsx @@ -68,8 +68,8 @@ export const UnitHeader = ({
{obj.name &&

{obj.name}

} - {obj.application && - {providerInfo(obj.application.data.metaDataFields["name:en"])}} + {obj.applicationName && + {obj.applicationName}} {(obj.description && displayDescription) && } diff --git a/client/src/components/UnitHeader.scss b/client/src/components/UnitHeader.scss index 5061ba51..6c7fc986 100644 --- a/client/src/components/UnitHeader.scss +++ b/client/src/components/UnitHeader.scss @@ -58,6 +58,7 @@ max-height: 300px; max-width: 300px; border-radius: 5px; + margin-bottom: auto; } &.small { diff --git a/client/src/locale/en.js b/client/src/locale/en.js index 4485eb14..7bedb484 100644 --- a/client/src/locale/en.js +++ b/client/src/locale/en.js @@ -101,14 +101,18 @@ const en = { expiryDays: "Expiry days" }, roles: { - title: "Roles", + title: "Access Roles", + applicationName:"Application", + accessRole:"Name", name: "Name", namePlaceHolder: "The name of the role", shortName: "Short name", landingPage: "Website", + userRoleCount: "# Users", landingPagePlaceHolder: "https://landingpage.com", defaultExpiryDays: "Expiry days", endDate: "End date", + noEndDate: "No end date", authority: "Authority", yourRole: "Your role", description: "Description", @@ -140,7 +144,8 @@ const en = { searchPlaceHolder: "Search for user roles...", noResults: "No user roles where found", notAllowed: "You're not allowed to delete this user role because of missing roles", - updateConfirmation: "Are you sure you want to change the end date of role {{roleName}} for {{userName}}", + updateConfirmation: "Are you sure you want to change the end date of role {{roleName}} for {{userName}}", + updateConfirmationRemoveEndDate: "Are you sure you want to remove the end date of role {{roleName}} for {{userName}}", updateFlash: "The end date for role {{roleName}} has been updated", deleteConfirmation: "Are you sure you want to remove this role from this user(s)?", deleteFlash: "User role(s) have been removed", @@ -163,6 +168,7 @@ const en = { newInvite: "New invite", newGuest: "Invite guest", invitees: "Invitees", + intendedRoles: "Roles", inviteesPlaceholder: "Invitee email addresses", requiredEmail: "At least one email is required", requiredRole: "At least one role is required for an invitation", diff --git a/client/src/pages/Role.js b/client/src/pages/Role.js index 0ad3b820..ad38efd2 100644 --- a/client/src/pages/Role.js +++ b/client/src/pages/Role.js @@ -48,14 +48,14 @@ export const Role = () => { setRole(res[0]); setLoading(false); const newTabs = [ - userRole.authority !== "GUEST")}/> - , + : null, { preloadedInvitations={res[2]}/> ]; - setTabs(newTabs); + setTabs(newTabs.filter(tab => tab !== null)); }) .catch(() => navigate("/")) diff --git a/client/src/tabs/Invitations.js b/client/src/tabs/Invitations.js index b3aac7f9..671e615d 100644 --- a/client/src/tabs/Invitations.js +++ b/client/src/tabs/Invitations.js @@ -191,7 +191,7 @@ export const Invitations = ({role, preloadedInvitations, standAlone = false}) => }, { key: "intendedRoles", - header: I18n.t("roles.title"), + header: I18n.t("invitations.intendedRoles"), mapper: invitation => invitation.intendedRoles }, { diff --git a/client/src/tabs/Roles.js b/client/src/tabs/Roles.js index 9af97dd0..72d78fbe 100644 --- a/client/src/tabs/Roles.js +++ b/client/src/tabs/Roles.js @@ -3,14 +3,12 @@ import {useAppStore} from "../stores/AppStore"; import React, {useEffect, useState} from "react"; import {Entities} from "../components/Entities"; import I18n from "../locale/I18n"; -import {Chip, Loader, Tooltip} from "@surfnet/sds"; +import {Chip, Loader} from "@surfnet/sds"; import {useNavigate} from "react-router-dom"; import {AUTHORITIES, isUserAllowed, markAndFilterRoles} from "../utils/UserRole"; import {rolesByApplication, searchRoles} from "../api"; import {isEmpty, stopEvent} from "../utils/Utils"; import debounce from "lodash.debounce"; -import {dateFromEpoch} from "../utils/Date"; -import {ReactComponent as RoleIcon} from "@surfnet/sds/icons/illustrative-icons/hierarchy.svg"; import {chipTypeForUserRole} from "../utils/Authority"; export const Roles = () => { @@ -77,32 +75,25 @@ export const Roles = () => {
) } - const landingPage = role => { - const url = role.isUserRole ? role.role.landingPage : role.landingPage; - if (isEmpty(url)) { - return ""; - } - return {url} - } - const columns = [ { nonSortable: true, - key: "icon", + key: "logo", header: "", - mapper: role =>
- } - tip={I18n.t("tooltips.roleIcon", - { - name: role.name, - createdAt: dateFromEpoch(role.isUserRole ? role.createdAt : role.auditable.createdAt) - })}/> -
+ mapper: role => { + return
+ logo +
+ } + }, + { + key: "applicationName", + header: I18n.t("roles.applicationName"), + mapper: role => {role.applicationName} }, { key: "name", - header: I18n.t("roles.name"), + header: I18n.t("roles.accessRole"), mapper: role => {role.name} }, { @@ -113,20 +104,14 @@ export const Roles = () => { { key: "authority", header: I18n.t("roles.authority"), - mapper: role => - }, - { - key: "defaultExpiryDays", - header: I18n.t("users.expiryDays"), - mapper: role => role.isUserRole ? role.role.defaultExpiryDays : role.defaultExpiryDays + mapper: role => }, { - key: "landingPage", - header: I18n.t("roles.landingPage"), - hasLink: true, - mapper: role => landingPage(role) + key: "userRoleCount", + header: I18n.t("roles.userRoleCount"), + mapper: role => role.userRoleCount } ]; @@ -140,22 +125,23 @@ export const Roles = () => { return (
- !(role.isUserRole && role.authority === "GUEST"))} - modelName="roles" - showNew={isManager} - newLabel={I18n.t("roles.new")} - newEntityPath={"/role/new"} - defaultSort="name" - columns={columns} - searchAttributes={["name", "description", "landingPage"]} - customNoEntities={I18n.t(`roles.noResults`)} - loading={false} - inputFocus={true} - hideTitle={true} - filters={moreToShow && moreResultsAvailable()} - customSearch={roleSearchRequired && isSuperUser ? search : null} - rowLinkMapper={isUserAllowed(AUTHORITIES.INVITER, user) ? openRole : null} - busy={searching}/> + !(role.isUserRole && role.authority === "GUEST"))} + modelName="roles" + showNew={isManager} + newLabel={I18n.t("roles.new")} + newEntityPath={"/role/new"} + defaultSort="name" + columns={columns} + searchAttributes={["name", "description", "applicationName"]} + customNoEntities={I18n.t(`roles.noResults`)} + loading={false} + inputFocus={true} + hideTitle={false} + filters={moreToShow && moreResultsAvailable()} + customSearch={roleSearchRequired && isSuperUser ? search : null} + rowLinkMapper={isUserAllowed(AUTHORITIES.INVITER, user) ? openRole : null} + busy={searching}/>
); diff --git a/client/src/tabs/Roles.scss b/client/src/tabs/Roles.scss index cc5666be..5ec7490f 100644 --- a/client/src/tabs/Roles.scss +++ b/client/src/tabs/Roles.scss @@ -6,6 +6,10 @@ div.mod-roles { thead { th { + &.applicationName { + width: 15%; + } + &.name { width: 15%; } @@ -19,19 +23,16 @@ div.mod-roles { text-align: center; } - &.defaultExpiryDays { + &.userRoleCount { width: 15%; text-align: center; } - &.landingPage { - width: 25%; - } } } tbody { - td.authority, td.defaultExpiryDays { + td.authority, td.defaultExpiryDays, td.userRoleCount { text-align: center; } diff --git a/client/src/tabs/UserRoles.js b/client/src/tabs/UserRoles.js index 5c626413..db4c77fe 100644 --- a/client/src/tabs/UserRoles.js +++ b/client/src/tabs/UserRoles.js @@ -60,7 +60,7 @@ export const UserRoles = ({role, guests, userRoles}) => { setConfirmation({ cancel: () => setConfirmationOpen(false), action: () => doUpdateEndDate(userRole, newEndDate, false), - question: I18n.t("userRoles.updateConfirmation", { + question: I18n.t(`userRoles.${isEmpty(newEndDate) ? "updateConfirmationRemoveEndDate":"updateConfirmation"}`, { roleName: userRole.role.name, userName: userRole.userInfo.name }), @@ -125,14 +125,18 @@ export const UserRoles = ({role, guests, userRoles}) => { }; const displayEndDate = userRole => { - if (allowedToRenewUserRole(user, userRole) && userRole.endDate) { + if (allowedToRenewUserRole(user, userRole)) { return (
+ {!userRole.endDate && + + {I18n.t("roles.noEndDate")} + } doUpdateEndDate(userRole, date, true)} - allowNull={false} + allowNull={true} showYearDropdown={true} />
@@ -231,7 +235,7 @@ export const UserRoles = ({role, guests, userRoles}) => { modelName="userRoles" defaultSort="name" columns={columns} - newLabel={I18n.t(guests ? "invitations.newGuest": "invitations.new")} + newLabel={I18n.t(guests ? "invitations.newGuest" : "invitations.new")} showNew={true} newEntityFunc={() => navigate("/invitation/new", {state: role.id})} customNoEntities={I18n.t(`userRoles.noResults`)} diff --git a/client/src/tabs/UserRoles.scss b/client/src/tabs/UserRoles.scss index 9708ddea..848c1f74 100644 --- a/client/src/tabs/UserRoles.scss +++ b/client/src/tabs/UserRoles.scss @@ -2,10 +2,18 @@ .date-field-container { display: flex; + position: relative; .date-field { margin-left: auto; } + + .no-end-date { + position: absolute; + z-index: 99; + top: 10px; + right: 130px; + } } table.userRoles { diff --git a/client/src/utils/Manage.js b/client/src/utils/Manage.js index d54f6042..075dd149 100644 --- a/client/src/utils/Manage.js +++ b/client/src/utils/Manage.js @@ -1,4 +1,5 @@ import {isEmpty} from "./Utils"; +import I18n from "../locale/I18n"; export const singleProviderToOption = provider => { @@ -15,6 +16,16 @@ export const providersToOptions = providers => { return providers.map(provider => singleProviderToOption(provider)); } +export const deriveApplicationAttributes = role => { + const application = role.application; + if (!isEmpty(application)) { + const metaData = application.data.metaDataFields; + role.applicationName = metaData[`name:${I18n.locale}`] || metaData["name:en"] + role.applicationOrganizationName = metaData[`OrganizationName:${I18n.locale}`] || metaData["OrganizationName:en"]; + role.logo = metaData["logo:0:url"] ; + } +} + export const providerInfo = provider => { if (isEmpty(provider)) { diff --git a/client/src/utils/UserRole.js b/client/src/utils/UserRole.js index 1504ce4e..9da85f22 100644 --- a/client/src/utils/UserRole.js +++ b/client/src/utils/UserRole.js @@ -1,4 +1,6 @@ import {isEmpty} from "./Utils"; +import {deriveApplicationAttributes} from "./Manage"; +import I18n from "../locale/I18n"; export const INVITATION_STATUS = { OPEN: "OPEN", @@ -81,8 +83,13 @@ export const allowedToRenewUserRole = (user, userRole) => { export const urnFromRole = (groupUrnPrefix, role) => `${groupUrnPrefix}:${role.manageId}:${role.shortName}`; -//TODO this now has two usages. Showing all roles in the roles for your overview and in new invitation - refactor to two export const markAndFilterRoles = (user, allRoles) => { + allRoles.forEach(role => { + role.isUserRole = false; + role.label = role.name; + role.value = role.id; + deriveApplicationAttributes(role); + }); const userRoles = user.userRoles; userRoles.forEach(userRole => { userRole.isUserRole = true; @@ -94,12 +101,11 @@ export const markAndFilterRoles = (user, allRoles) => { userRole.defaultExpiryDays = userRole.role.defaultExpiryDays; userRole.eduIDOnly = userRole.role.eduIDOnly; userRole.enforceEmailEquality = userRole.role.enforceEmailEquality; + userRole.applicationName = userRole.role.applicationName; + userRole.applicationOrganizationName = userRole.role.applicationOrganizationName; + userRole.logo = userRole.role.logo; }) - allRoles.forEach(role => { - role.isUserRole = false; - role.label = role.name; - role.value = role.id; - }); + return allRoles .filter(role => userRoles.every(userRole => userRole.role.id !== role.id)) .concat(userRoles); diff --git a/server/src/main/java/access/api/RoleController.java b/server/src/main/java/access/api/RoleController.java index 1ee17277..74b1067d 100644 --- a/server/src/main/java/access/api/RoleController.java +++ b/server/src/main/java/access/api/RoleController.java @@ -57,7 +57,7 @@ public RoleController(Config config, public ResponseEntity> rolesByApplication(@Parameter(hidden = true) User user) { LOG.debug("/roles"); if (user.isSuperUser() && !config.isRoleSearchRequired()) { - return ResponseEntity.ok(roleRepository.findAll()); + return ResponseEntity.ok(manage.deriveRemoteApplications(roleRepository.findAll())); } UserPermissions.assertAuthority(user, Authority.MANAGER); Set manageIdentifiers = user.getUserRoles().stream() @@ -65,7 +65,7 @@ public ResponseEntity> rolesByApplication(@Parameter(hidden = true) U .filter(userRole -> userRole.getAuthority().hasEqualOrHigherRights(Authority.MANAGER)) .map(userRole -> userRole.getRole().getManageId()) .collect(Collectors.toSet()); - return ResponseEntity.ok(roleRepository.findByManageIdIn(manageIdentifiers)); + return ResponseEntity.ok(manage.deriveRemoteApplications(roleRepository.findByManageIdIn(manageIdentifiers))); } @GetMapping("{id}") @@ -84,7 +84,7 @@ public ResponseEntity> search(@RequestParam(value = "query") String q LOG.debug("/search"); UserPermissions.assertSuperUser(user); List roles = roleRepository.search(query + "*", 15); - return ResponseEntity.ok(roles); + return ResponseEntity.ok(manage.deriveRemoteApplications(roles)); } @PostMapping("validation/short_name") diff --git a/server/src/main/java/access/api/UserRoleController.java b/server/src/main/java/access/api/UserRoleController.java index 003c781c..f30bf26a 100644 --- a/server/src/main/java/access/api/UserRoleController.java +++ b/server/src/main/java/access/api/UserRoleController.java @@ -61,7 +61,7 @@ public ResponseEntity> byRole(@PathVariable("roleId") Long roleId public ResponseEntity> updateUserRoleExpirationDate(@Validated @RequestBody UpdateUserRole updateUserRole, @Parameter(hidden = true) User user) { UserRole userRole = userRoleRepository.findById(updateUserRole.getUserRoleId()).orElseThrow(NotFoundException::new); - if (!config.isPastDateAllowed() && Instant.now().isAfter(updateUserRole.getEndDate())) { + if (updateUserRole.getEndDate() != null && !config.isPastDateAllowed() && Instant.now().isAfter(updateUserRole.getEndDate())) { throw new NotAllowedException("End date must be after now"); } UserPermissions.assertValidInvitation(user, userRole.getAuthority(), List.of(userRole.getRole())); diff --git a/server/src/main/java/access/manage/Manage.java b/server/src/main/java/access/manage/Manage.java index 347b7c7c..4353ff39 100644 --- a/server/src/main/java/access/manage/Manage.java +++ b/server/src/main/java/access/manage/Manage.java @@ -1,8 +1,9 @@ package access.manage; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import access.model.Role; + +import java.util.*; +import java.util.stream.Collectors; public interface Manage { @@ -34,4 +35,21 @@ default Map addIdentifierAlias(Map provider) { return provider; } + default List deriveRemoteApplications(List roles) { + //First get all unique remote manage entities and group them by entityType + Map> groupedManageIdentifiers = roles.stream() + .map(role -> new ManageIdentifier(role.getManageId(), role.getManageType())) + .collect(Collectors.toSet()) + .stream() + .collect(Collectors.groupingBy(ManageIdentifier::entityType)); + //Now for each entityType (hopefully one, maximum two) we call manage and create a map with as key + Map> remoteApplications = groupedManageIdentifiers.entrySet().stream() + .map(entry -> this.providersByIdIn(entry.getKey(), entry.getValue().stream().map(ManageIdentifier::id).toList())) + .flatMap(List::stream) + .collect(Collectors.toMap(map -> (String) map.get("id"), map -> map)); + //Add the metadata to the role + roles.forEach(role -> role.setApplication(addIdentifierAlias(remoteApplications.get(role.getManageId())))); + return roles; + } + } diff --git a/server/src/main/java/access/model/Role.java b/server/src/main/java/access/model/Role.java index 506e8134..1ade6dab 100644 --- a/server/src/main/java/access/model/Role.java +++ b/server/src/main/java/access/model/Role.java @@ -10,6 +10,7 @@ import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; +import org.hibernate.annotations.Formula; import java.io.Serializable; import java.util.*; @@ -57,6 +58,8 @@ public class Role implements Serializable, Provisionable { @Column(name = "block_expiry_date") private boolean blockExpiryDate; + @Formula(value = "(SELECT COUNT(*) FROM user_roles ur WHERE ur.role_id=id)") + private Long userRoleCount; @OneToMany(mappedBy = "role", orphanRemoval = true, diff --git a/server/src/main/java/access/model/UpdateUserRole.java b/server/src/main/java/access/model/UpdateUserRole.java index 215dc6c3..d8a41f5e 100644 --- a/server/src/main/java/access/model/UpdateUserRole.java +++ b/server/src/main/java/access/model/UpdateUserRole.java @@ -18,7 +18,6 @@ public class UpdateUserRole { @NotNull private Long userRoleId; - @NotNull private Instant endDate; } diff --git a/welcome/src/components/RoleCard.scss b/welcome/src/components/RoleCard.scss index eea2269a..3f09359c 100644 --- a/welcome/src/components/RoleCard.scss +++ b/welcome/src/components/RoleCard.scss @@ -11,10 +11,23 @@ left: -22px; } + @keyframes glow { + 0% { + box-shadow: 1px 0.13rem 0.13rem 0 #FFD700; + border: 0.0625rem solid #FFD700; + } + 50% { + box-shadow: 1px 0.16rem 0.16rem 0 #eac602; + border: 0.0625rem solid #eac602; + } + 100% { + box-shadow: 1px 0.13rem 0.13rem 0 #FFD700; + border: 0.0625rem solid #FFD700; + } + } + .sds--card { - box-shadow: 0 0.2rem 0.2rem 0 var(--sds--color--blue--400); - background-color: var(--sds--color--gray--100); - border: 0.0625rem solid var(--sds--color--blue--500); + animation: glow 2.5s infinite; } } diff --git a/welcome/src/components/User.js b/welcome/src/components/User.js index dd36b541..76780f4b 100644 --- a/welcome/src/components/User.js +++ b/welcome/src/components/User.js @@ -20,12 +20,12 @@ export const User = ({user, invitationRoles = []}) => { .filter(userRole => !rolesToExclude.includes(userRole.role.id)); return ( <> - {isEmpty(user.userRoles) &&

{I18n.t("users.noRolesInfo")}

} + {(isEmpty(user.userRoles) && isEmpty(invitationRoles))&&

{I18n.t("users.noRolesInfo")}

} {!isEmpty(user.userRoles) && <> {filteredUserRoles .map((userRole, index) => renderUserRole(userRole, index))} - {filteredUserRoles.length === 0 && + {(isEmpty(user.userRoles) && isEmpty(invitationRoles)) &&

{I18n.t(`users.noRolesFound`)}

} } diff --git a/welcome/src/pages/App.js b/welcome/src/pages/App.js index 05e74fb6..13670165 100644 --- a/welcome/src/pages/App.js +++ b/welcome/src/pages/App.js @@ -55,7 +55,7 @@ export const App = () => { .then(res => { useAppStore.setState(() => ({user: res, authenticated: true})); const location = localStorage.getItem("location") || window.location.pathname + window.location.search; - const newLocation = location.startsWith("/login") ? "/home" : location; + const newLocation = location.startsWith("/login") ? "/profile" : location; localStorage.removeItem("location"); setLoading(false); navigate(newLocation);