Skip to content

Commit

Permalink
Users full name and orcid is shown and filterable during the addition…
Browse files Browse the repository at this point in the history
… of collaborators to a project (#707)

* Provide specifications in user context to enable filtering by full name user name and orcid

* Extend userinfo with optional orcid if provided during registration

* Update add collaborator UI to show orcid, full name and filtering of the new values

* Overhaul UI and ProjectAccessComponentgrid

* Add confirm notification if a project user is to be removed

* ƒilter for oidc instead of orcid specifically

* Update userinfo to also contain oidc issuer

* Show oidc link as specified by orcId and generalize it so multiple oidc could be shown

* Update css to use oidc instead of orcid

* User to userinformation conversion provides oidc issuer

* Extract oidcLogo and oidctype to make it reusable

* LoginLayout employs extracted oidc logo

* Provide javadocs for OidcLogo and extract duplicate css into singular default value for oidc logo

* Move OidcLogo and OidcType into general components and adjust width for user access grid

* Adjust enum naming scheme to adhere to programmatic standard

* Allow filtering of oidcissuer as well within add collaborator dialog

* Don't throw an exception if unknown oidc is provided and seperate setting of Oidc with generation of Info item

* Fix measurement spacing

* Remove controls if user doesn't have access

* Adapt frontend cell and create grid via method instead of modifying it

* Remove unnecessary semicolon

* Add private final to header object

* Employ same Div for selection and showing user info

* Update CSS according to SP recommendations

---------

Co-authored-by: Tobias Koch <[email protected]>
  • Loading branch information
Steffengreiner and KochTobi authored Jul 29, 2024
1 parent 3225958 commit 5437054
Show file tree
Hide file tree
Showing 20 changed files with 461 additions and 162 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
* @since 1.0.0
*/
public record UserInfo(String id, String fullName, String emailAddress, String platformUserName,
boolean isActive) implements Serializable {
boolean isActive, String oidcId, String oidcIssuer) implements Serializable {

}
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,6 @@ public interface UserInformationService {

Optional<UserInfo> findByOidc(String oidcId, String oidcIssuer);

List<UserInfo> findAllActive(String filter, int offset, int limit, List<SortOrder> sortOrders);
List<UserInfo> queryActiveUsersWithFilter(String filter, int offset, int limit,
List<SortOrder> sortOrders);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import life.qbic.identity.domain.model.UserId;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;

/**
Expand All @@ -21,7 +22,8 @@
*
* @since 1.0.0
*/
public interface QbicUserRepo extends JpaRepository<User, UserId> {
public interface QbicUserRepo extends JpaRepository<User, UserId>,
JpaSpecificationExecutor<User> {

/**
* Find users by mail address in the persistent data storage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import life.qbic.identity.domain.repository.UserDataStorage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Component;


Expand Down Expand Up @@ -60,13 +61,70 @@ public Optional<User> findUserByUserName(String userName) {
}

@Override
public List<User> findByUserNameContainingIgnoreCaseAndActiveTrue(String userName,
Pageable pageable) {
return userRepo.findAllByUserNameContainingIgnoreCaseAndActiveTrue(userName, pageable);
public List<User> queryActiveUsersWithFilter(String filter, Pageable pageable) {
Specification<User> userSpecification = generateUserFilterSpecification(filter);
return userRepo.findAll(userSpecification, pageable).getContent();
}

@Override
public Optional<User> findByOidcIdEqualsAndOidcIssuerEquals(String oidcId, String oidcIssuer) {
return userRepo.findByOidcIdEqualsAndOidcIssuerEquals(oidcId, oidcIssuer);
}

private Specification<User> generateUserFilterSpecification(String filter) {
Specification<User> isBlankSpec = UserSpec.isBlank(filter);
Specification<User> isFullName = UserSpec.isFullName(filter);
Specification<User> isUserNameSpec = UserSpec.isUserName(filter);
Specification<User> isOidc = UserSpec.isOidc(filter);
Specification<User> isOidcIssuer = UserSpec.isOidcIssuer(filter);
Specification<User> isActiveSpec = UserSpec.isActive();
Specification<User> filterSpecification =
Specification.anyOf(isFullName,
isUserNameSpec,
isOidc,
isOidcIssuer
);
return Specification.where(isBlankSpec)
.and(filterSpecification)
.and(isActiveSpec);
}

private static class UserSpec {

//If no filter was provided return all Users
public static Specification<User> isBlank(String filter) {
return (root, query, builder) -> {
if (filter != null && filter.isBlank()) {
return builder.conjunction();
}
return null;
};
}

public static Specification<User> isUserName(String filter) {
return (root, query, builder) ->
builder.like(root.get("userName"), "%" + filter + "%");
}

public static Specification<User> isFullName(String filter) {
return (root, query, builder) ->
builder.like(root.get("fullName"), "%" + filter + "%");
}

// Should be extended if additional oidc providers are included, for now we only work with orcid
public static Specification<User> isOidc(String filter) {
return (root, query, builder) ->
builder.like(root.get("oidcId"), "%" + filter + "%");
}

public static Specification<User> isOidcIssuer(String filter) {
return (root, query, builder) ->
builder.like(root.get("oidcIssuer"), "%" + filter + "%");
}

public static Specification<User> isActive() {
return (root, query, builder) ->
builder.isTrue(root.get("active"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public boolean isEmailAvailable(String email) {
}

@Override
public List<UserInfo> findAllActive(String filter, int offset, int limit,
public List<UserInfo> queryActiveUsersWithFilter(String filter, int offset, int limit,
List<SortOrder> sortOrders) {
List<Order> orders = sortOrders.stream().map(it -> {
Order order;
Expand All @@ -93,18 +93,19 @@ public List<UserInfo> findAllActive(String filter, int offset, int limit,
}
return order;
}).toList();
return userRepository.findByUserNameContainingIgnoreCaseAndActiveTrue(
return userRepository.queryActiveUsersWithFilter(
filter, new OffsetBasedRequest(offset, limit, Sort.by(orders)))
.stream()
.map(user -> new UserInfo(user.id().get(), user.fullName().get(), user.emailAddress().get(),
user.userName(), user.isActive()))
user.userName(), user.isActive(), user.getOidcId().orElse(null),
user.getOidcIssuer().orElse(null)))
.toList();
}

private UserInfo convert(User user) {
return new UserInfo(user.id().get(), user.fullName().get(), user.emailAddress().get(),
user.userName(),
user.isActive());
user.isActive(), user.getOidcId().orElse(null), user.getOidcIssuer().orElse(null));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public interface UserDataStorage {

Optional<User> findUserByUserName(String userName);

List<User> findByUserNameContainingIgnoreCaseAndActiveTrue(String username, Pageable pageable);
List<User> queryActiveUsersWithFilter(String filter, Pageable pageable);

Optional<User> findByOidcIdEqualsAndOidcIssuerEquals(String oidcId, String oidcIssuer);
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ public void addUser(User user) throws UserStorageException {
saveUser(user);
}

public List<User> findByUserNameContainingIgnoreCaseAndActiveTrue(String userName,
public List<User> queryActiveUsersWithFilter(String filter,
Pageable pageable) {
return dataStorage.findByUserNameContainingIgnoreCaseAndActiveTrue(userName, pageable);
return dataStorage.queryActiveUsersWithFilter(filter, pageable);
}

public Optional<User> findByOidc(String oidcId, String oidcIssuer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ Since we want to remove the spacing between the cancel and confirm button we rep
}

.add-user-to-project-dialog::part(overlay) {
height: fit-content;
min-width: fit-content;
height: clamp(700px, 100%, 700px);
width: clamp(700px, 100%, 700px);
}

.add-user-to-project-dialog::part(content) {
Expand Down
32 changes: 32 additions & 0 deletions user-interface/frontend/themes/datamanager/components/div.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,38 @@
flex-flow: row wrap;
}

.user-info-component {
display: flex;
align-items: center;
gap: var(--lumo-space-l);
margin: var(--lumo-space-s);
}

.user-info-component .avatar {
width: var(--lumo-icon-size-l);
height: var(--lumo-icon-size-l);
}

.user-info-component .user-info {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: var(--lumo-space-s);
}

.user-info-component .user-info .oidc {
display: inline-flex;
align-items: center;
gap: var(--lumo-space-s);
white-space: nowrap;
}

.user-info-component .user-info .user-name-and-full-name {
display: flex;
gap: var(--lumo-space-s);
align-items: baseline;
}

.disclaimer {
display: flex;
justify-content: center;
Expand Down
11 changes: 11 additions & 0 deletions user-interface/frontend/themes/datamanager/components/image.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
img.clickable {
cursor: pointer;
}

/*Default height and width for the oidc logo */
.oidc-logo {
width: var(--lumo-icon-size-m);
height: var(--lumo-icon-size-m);
}

.size-small {
width: var(--lumo-icon-size-s);
height: var(--lumo-icon-size-s);
}
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
flex-direction: column;
row-gap: var(--lumo-space-m);
padding: var(--lumo-space-m);
justify-content: space-between;
}

.main.measurement .measurement-main-content .title {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,8 +382,7 @@
display: flex;
row-gap: var(--lumo-space-l);
flex-direction: column;
width: clamp(500px, 70%, 100%);
height: max-content;
height: auto;
}

.project-access-component .header {
Expand All @@ -398,6 +397,12 @@
align-items: center;
}

.project-access-component .oidc-cell {
display: inline-flex;
gap: var(--lumo-space-s);
align-items: center;
}

.project-collection-component {
height: 100%;
/*Necessary since we currently don't distinguish
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,6 @@
cursor: pointer;
}

.login-card .logo {
height: var(--lumo-icon-size-m);
width: var(--lumo-icon-size-m);
}

.login-card .text {
font-weight: 500;
font-size: var(--lumo-font-size-m);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import org.springframework.stereotype.Component;

/**
* A details service loading user detais for OpenId Connect users known to the system.
* A details service loading user details for OpenId Connect users known to the system.
*/
@Component
public class OidcUserDetailsService extends OidcUserService {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package life.qbic.datamanager.views.general.oidc;

import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.server.AbstractStreamResource;
import com.vaadin.flow.server.StreamResource;

/**
* OidcLogo shown within the data manager application.
* Logo source and image path is based on the information provided within the {@link OidcType}
*/
public class OidcLogo extends Image {

private final OidcType oidcType;

public OidcLogo(OidcType oidcType) {
this.oidcType = oidcType;
addClassName("oidc-logo");
setSrc(getLogoResource());
}

private AbstractStreamResource getLogoResource() {
String oidcLogoSrc = oidcType.getLogoPath();
//Image source cannot contain a "/" so we look for the actual file name independent in which folder path it is contained.
if (oidcLogoSrc.contains("/")) {
oidcLogoSrc = oidcType.getLogoPath().substring(oidcType.getLogoPath().lastIndexOf('/') + 1);
}
return new StreamResource(oidcLogoSrc,
() -> getClass().getClassLoader().getResourceAsStream(oidcType.getLogoPath()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package life.qbic.datamanager.views.general.oidc;

/**
* The Oidc Types enum contains the associated logo path, the issuer and the url of the
* corresponding oidc a user has provided
*/
public enum OidcType {
ORCID("https://orcid.org", "login/orcid_logo.svg", "https://orcid.org/", "Orcid"),
ORCID_SANDBOX("https://sandbox.orcid.org", "login/orcid_logo.svg", "https://sandbox.orcid.org/",
"OrcId");

private final String issuer;
private final String logoPath;
private final String url;
private final String name;

OidcType(String issuer, String logoPath, String url, String name) {
this.issuer = issuer;
this.logoPath = logoPath;
this.url = url;
this.name = name;
}

public String getIssuer() {
return issuer;
}

public String getLogoPath() {
return logoPath;
}

public String getUrl() {
return url;
}

public String getName() {
return name;
}
}
Loading

0 comments on commit 5437054

Please sign in to comment.