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 (#770)
  • Loading branch information
sven1103 authored Aug 13, 2024
1 parent a63d57e commit 19cedd5
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 19cedd5

Please sign in to comment.