diff --git a/identity-api/src/main/java/life/qbic/identity/api/UserInfo.java b/identity-api/src/main/java/life/qbic/identity/api/UserInfo.java index 6f281c390..8cec538d0 100644 --- a/identity-api/src/main/java/life/qbic/identity/api/UserInfo.java +++ b/identity-api/src/main/java/life/qbic/identity/api/UserInfo.java @@ -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 { } diff --git a/identity-api/src/main/java/life/qbic/identity/api/UserInformationService.java b/identity-api/src/main/java/life/qbic/identity/api/UserInformationService.java index 7a101f17a..c37fde52b 100644 --- a/identity-api/src/main/java/life/qbic/identity/api/UserInformationService.java +++ b/identity-api/src/main/java/life/qbic/identity/api/UserInformationService.java @@ -55,5 +55,6 @@ public interface UserInformationService { Optional findByOidc(String oidcId, String oidcIssuer); - List findAllActive(String filter, int offset, int limit, List sortOrders); + List queryActiveUsersWithFilter(String filter, int offset, int limit, + List sortOrders); } diff --git a/identity-infrastructure/src/main/java/life/qbic/identity/infrastructure/QbicUserRepo.java b/identity-infrastructure/src/main/java/life/qbic/identity/infrastructure/QbicUserRepo.java index e96cfb73e..b7d5958c5 100644 --- a/identity-infrastructure/src/main/java/life/qbic/identity/infrastructure/QbicUserRepo.java +++ b/identity-infrastructure/src/main/java/life/qbic/identity/infrastructure/QbicUserRepo.java @@ -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; /** @@ -21,7 +22,8 @@ * * @since 1.0.0 */ -public interface QbicUserRepo extends JpaRepository { +public interface QbicUserRepo extends JpaRepository, + JpaSpecificationExecutor { /** * Find users by mail address in the persistent data storage diff --git a/identity-infrastructure/src/main/java/life/qbic/identity/infrastructure/UserJpaRepository.java b/identity-infrastructure/src/main/java/life/qbic/identity/infrastructure/UserJpaRepository.java index 18dbe3cf6..8a35279a8 100644 --- a/identity-infrastructure/src/main/java/life/qbic/identity/infrastructure/UserJpaRepository.java +++ b/identity-infrastructure/src/main/java/life/qbic/identity/infrastructure/UserJpaRepository.java @@ -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; @@ -60,13 +61,70 @@ public Optional findUserByUserName(String userName) { } @Override - public List findByUserNameContainingIgnoreCaseAndActiveTrue(String userName, - Pageable pageable) { - return userRepo.findAllByUserNameContainingIgnoreCaseAndActiveTrue(userName, pageable); + public List queryActiveUsersWithFilter(String filter, Pageable pageable) { + Specification userSpecification = generateUserFilterSpecification(filter); + return userRepo.findAll(userSpecification, pageable).getContent(); } @Override public Optional findByOidcIdEqualsAndOidcIssuerEquals(String oidcId, String oidcIssuer) { return userRepo.findByOidcIdEqualsAndOidcIssuerEquals(oidcId, oidcIssuer); } + + private Specification generateUserFilterSpecification(String filter) { + Specification isBlankSpec = UserSpec.isBlank(filter); + Specification isFullName = UserSpec.isFullName(filter); + Specification isUserNameSpec = UserSpec.isUserName(filter); + Specification isOidc = UserSpec.isOidc(filter); + Specification isOidcIssuer = UserSpec.isOidcIssuer(filter); + Specification isActiveSpec = UserSpec.isActive(); + Specification 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 isBlank(String filter) { + return (root, query, builder) -> { + if (filter != null && filter.isBlank()) { + return builder.conjunction(); + } + return null; + }; + } + + public static Specification isUserName(String filter) { + return (root, query, builder) -> + builder.like(root.get("userName"), "%" + filter + "%"); + } + + public static Specification 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 isOidc(String filter) { + return (root, query, builder) -> + builder.like(root.get("oidcId"), "%" + filter + "%"); + } + + public static Specification isOidcIssuer(String filter) { + return (root, query, builder) -> + builder.like(root.get("oidcIssuer"), "%" + filter + "%"); + } + + public static Specification isActive() { + return (root, query, builder) -> + builder.isTrue(root.get("active")); + } + } } diff --git a/identity/src/main/java/life/qbic/identity/application/service/BasicUserInformationService.java b/identity/src/main/java/life/qbic/identity/application/service/BasicUserInformationService.java index 131f7dc5b..4cb985fea 100644 --- a/identity/src/main/java/life/qbic/identity/application/service/BasicUserInformationService.java +++ b/identity/src/main/java/life/qbic/identity/application/service/BasicUserInformationService.java @@ -82,7 +82,7 @@ public boolean isEmailAvailable(String email) { } @Override - public List findAllActive(String filter, int offset, int limit, + public List queryActiveUsersWithFilter(String filter, int offset, int limit, List sortOrders) { List orders = sortOrders.stream().map(it -> { Order order; @@ -93,18 +93,19 @@ public List 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 diff --git a/identity/src/main/java/life/qbic/identity/domain/repository/UserDataStorage.java b/identity/src/main/java/life/qbic/identity/domain/repository/UserDataStorage.java index e521ed99c..1ed8e7b01 100644 --- a/identity/src/main/java/life/qbic/identity/domain/repository/UserDataStorage.java +++ b/identity/src/main/java/life/qbic/identity/domain/repository/UserDataStorage.java @@ -58,7 +58,7 @@ public interface UserDataStorage { Optional findUserByUserName(String userName); - List findByUserNameContainingIgnoreCaseAndActiveTrue(String username, Pageable pageable); + List queryActiveUsersWithFilter(String filter, Pageable pageable); Optional findByOidcIdEqualsAndOidcIssuerEquals(String oidcId, String oidcIssuer); } diff --git a/identity/src/main/java/life/qbic/identity/domain/repository/UserRepository.java b/identity/src/main/java/life/qbic/identity/domain/repository/UserRepository.java index db3595ce0..240880815 100644 --- a/identity/src/main/java/life/qbic/identity/domain/repository/UserRepository.java +++ b/identity/src/main/java/life/qbic/identity/domain/repository/UserRepository.java @@ -110,9 +110,9 @@ public void addUser(User user) throws UserStorageException { saveUser(user); } - public List findByUserNameContainingIgnoreCaseAndActiveTrue(String userName, + public List queryActiveUsersWithFilter(String filter, Pageable pageable) { - return dataStorage.findByUserNameContainingIgnoreCaseAndActiveTrue(userName, pageable); + return dataStorage.queryActiveUsersWithFilter(filter, pageable); } public Optional findByOidc(String oidcId, String oidcIssuer) { diff --git a/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/sample/SamplePreviewJpaRepository.java b/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/sample/SamplePreviewJpaRepository.java index 8e0b4b367..c68766542 100644 --- a/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/sample/SamplePreviewJpaRepository.java +++ b/project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/sample/SamplePreviewJpaRepository.java @@ -68,6 +68,7 @@ private Specification generateExperimentIdandFilterSpecification( Specification isBlankSpec = SamplePreviewSpecs.isBlank(filter); Specification experimentIdSpec = SamplePreviewSpecs.experimentIdEquals( experimentId); + Specification organismIdSpec = SamplePreviewSpecs.organismIdContains(filter); Specification sampleCodeSpec = SamplePreviewSpecs.sampleCodeContains(filter); Specification sampleLabelSpec = SamplePreviewSpecs.sampleLabelContains(filter); Specification batchLabelSpec = SamplePreviewSpecs.batchLabelContains(filter); @@ -79,7 +80,7 @@ private Specification generateExperimentIdandFilterSpecification( filter); Specification commentSpec = SamplePreviewSpecs.commentContains(filter); Specification containsFilterSpec = Specification.anyOf(sampleCodeSpec, - sampleLabelSpec, batchLabelSpec, conditionSpec, speciesSpec, + sampleLabelSpec, organismIdSpec, batchLabelSpec, conditionSpec, speciesSpec, specimenSpec, analyteSpec, analysisMethodContains, commentSpec); Specification isDistinctSpec = SamplePreviewSpecs.isDistinct(); return Specification.where(experimentIdSpec).and(isBlankSpec) @@ -157,6 +158,11 @@ private static Specification ontologyColumnContains(String col, S }; } + public static Specification organismIdContains(String filter) { + return (root, query, builder) -> + builder.like(root.get("organismId"), "%" + filter + "%"); + } + public static Specification speciesContains(String filter) { return ontologyColumnContains("species", filter); } diff --git a/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/validation/MeasurementNGSValidator.java b/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/validation/MeasurementNGSValidator.java index fc8182b4e..b07144d85 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/validation/MeasurementNGSValidator.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/validation/MeasurementNGSValidator.java @@ -118,7 +118,7 @@ public ValidationResult validateUpdate(NGSMeasurementMetadata metadata, ProjectI public enum NGS_PROPERTY { QBIC_SAMPLE_ID("qbic sample id"), - SAMPLE_LABEL("sample label"), + SAMPLE_LABEL("sample name"), ORGANISATION_ID("organisation id"), FACILITY("facility"), INSTRUMENT("instrument"), diff --git a/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidator.java b/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidator.java index 35489efab..71e11329e 100644 --- a/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidator.java +++ b/project-management/src/main/java/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidator.java @@ -129,7 +129,7 @@ public ValidationResult validateUpdate(ProteomicsMeasurementMetadata metadata, public enum PROTEOMICS_PROPERTY { QBIC_SAMPLE_ID("qbic sample id"), - SAMPLE_LABEL("sample label"), + SAMPLE_LABEL("sample name"), ORGANISATION_ID("organisation id"), FACILITY("facility"), INSTRUMENT("instrument"), diff --git a/project-management/src/test/groovy/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidatorSpec.groovy b/project-management/src/test/groovy/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidatorSpec.groovy index bbf1a3444..ee7b0f7b9 100644 --- a/project-management/src/test/groovy/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidatorSpec.groovy +++ b/project-management/src/test/groovy/life/qbic/projectmanagement/application/measurement/validation/MeasurementProteomicsValidatorSpec.groovy @@ -47,7 +47,7 @@ class MeasurementMeasurementProteomicsValidatorSpec extends Specification { final ProjectInformationService projectInformationService = Mock(ProjectInformationService.class) - final static List validPXPProperties = Collections.unmodifiableList(["qbic sample id", "sample label", "organisation id", "facility", "instrument", + final static List validPXPProperties = Collections.unmodifiableList(["qbic sample id", "sample name", "organisation id", "facility", "instrument", "sample pool group", "cycle/fraction name", "digestion method", "digestion enzyme", "enrichment method", "injection volume (uL)", "lc column", "lcms method", "labeling type", "label", "comment"]) diff --git a/user-interface/frontend/themes/datamanager/components/card.css b/user-interface/frontend/themes/datamanager/components/card.css index e99edfd35..c91a24ff0 100644 --- a/user-interface/frontend/themes/datamanager/components/card.css +++ b/user-interface/frontend/themes/datamanager/components/card.css @@ -48,22 +48,20 @@ opacity: 0.05; } -.experimental-group { - display: flex; - flex-direction: column; - flex: auto; - font-size: var(--lumo-font-size-s); - padding: var(--lumo-space-l); - max-width: 300px; -} - .card-collection { display: flex; align-content: space-evenly; flex-flow: column; gap: 1rem; - margin-left: 1.5rem; - margin-top: 1.5rem; + padding-left: 1.5rem; + padding-top: 1.5rem; +} + +.card-collection .experimental-group { + display: flex; + flex-direction: column; + font-size: var(--lumo-font-size-s); + padding: var(--lumo-space-l); } .card-collection .collection-title { @@ -79,11 +77,10 @@ } .card-collection .collection-content { - display: flex; - align-content: space-evenly; - flex-flow: row wrap; - gap: 1rem; - flex-direction: row; + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-auto-rows: auto; + grid-gap: 1rem; } .card-collection .collection-controls { @@ -93,29 +90,29 @@ gap: 0.5rem; } -.experimental-group .header { +.card-collection .experimental-group .header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: var(--lumo-space-m); } -.experimental-group .card-title { +.card-collection .experimental-group .card-title { color: var(--lumo-secondary-text-color); font-size: var(--lumo-font-size-m); font-weight: bold; - white-space: nowrap; margin-bottom: 0.5rem; } -.experimental-group .content { - display: inline-flex; +.card-collection .experimental-group .content { + display: flex; flex-wrap: wrap; gap: var(--lumo-space-m); margin-bottom: var(--lumo-space-m); + flex-direction: column; } -.experimental-group vaadin-icon { +.card-collection .experimental-group vaadin-icon { cursor: pointer; width: 1em; color: darkgrey; diff --git a/user-interface/frontend/themes/datamanager/components/dialog.css b/user-interface/frontend/themes/datamanager/components/dialog.css index b6a9aeebd..306dc7338 100644 --- a/user-interface/frontend/themes/datamanager/components/dialog.css +++ b/user-interface/frontend/themes/datamanager/components/dialog.css @@ -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) { diff --git a/user-interface/frontend/themes/datamanager/components/div.css b/user-interface/frontend/themes/datamanager/components/div.css index ef47a2e4b..e595e6494 100644 --- a/user-interface/frontend/themes/datamanager/components/div.css +++ b/user-interface/frontend/themes/datamanager/components/div.css @@ -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; diff --git a/user-interface/frontend/themes/datamanager/components/image.css b/user-interface/frontend/themes/datamanager/components/image.css index e0c846f4b..c5e34eb1c 100644 --- a/user-interface/frontend/themes/datamanager/components/image.css +++ b/user-interface/frontend/themes/datamanager/components/image.css @@ -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); +} diff --git a/user-interface/frontend/themes/datamanager/components/main.css b/user-interface/frontend/themes/datamanager/components/main.css index b6daa17e3..672237c89 100644 --- a/user-interface/frontend/themes/datamanager/components/main.css +++ b/user-interface/frontend/themes/datamanager/components/main.css @@ -10,6 +10,7 @@ grid-template-areas: "content-area" "data-manager-footer"; + zoom: 90%; } #landing-page-layout .landing-page-content { @@ -71,8 +72,8 @@ } .main.experiment { - grid-template-columns: minmax(max-content, 100%); - grid-template-rows: auto; + grid-template-columns: 1fr; + grid-template-rows: 1fr; grid-template-areas: "experimentdetails"; } @@ -189,6 +190,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 { @@ -364,8 +366,8 @@ } .main.sample { - grid-template-columns: minmax(min-content, 60%) minmax(min-content, 50%); - grid-template-rows: minmax(min-content, 20%) minmax(min-content, 75%); + grid-template-columns: minmax(min-content, 50%) minmax(min-content, 50%); + grid-template-rows: minmax(min-content, 25%) minmax(min-content, 70%); grid-template-areas: ". batchdetails" "sampledetails sampledetails"; @@ -377,6 +379,43 @@ border-radius: var(--lumo-border-radius-m); } +/*We want to show disclaimer centralized over the whole main width and height independent of defined areas */ +.main.sample .no-samples-registered-disclaimer { + grid-column-start: 1; + grid-column-end: -1; + grid-row-start: 1; + grid-row-end: -1; +} + +/*We want to show disclaimer centralized over the whole main width and height independent of defined areas */ +.main.sample .no-experimental-groups-registered-disclaimer { + grid-column-start: 1; + grid-column-end: -1; + grid-row-start: 1; + grid-row-end: -1; +} + +.main.sample .sample-main-content { + display: flex; + flex-direction: column; + row-gap: var(--lumo-space-m); + padding: var(--lumo-space-m); + justify-content: space-between; +} + +.main.sample .sample-main-content .title { + font-weight: bold; + color: var(--lumo-secondary-text-color); + font-size: var(--lumo-font-size-xxl); + margin-bottom: 0.5rem; +} + +.main.sample .sample-main-content .buttonAndField { + display: inline-flex; + justify-content: space-between; + gap: var(--lumo-space-m); +} + .main.sample .batch-details-component { grid-area: batchdetails; } @@ -440,9 +479,10 @@ .main.sample { grid-template-columns: minmax(min-content, 1fr); grid-template-areas: + "." "batchdetails" "sampledetails"; - grid-template-rows: minmax(min-content, 20%) minmax(min-content, 75%); + grid-template-rows: minmax(min-content, 20%) minmax(min-content, 20%) minmax(min-content, 60%); } diff --git a/user-interface/frontend/themes/datamanager/components/page-area.css b/user-interface/frontend/themes/datamanager/components/page-area.css index 9e22c9b2e..7de4918e3 100644 --- a/user-interface/frontend/themes/datamanager/components/page-area.css +++ b/user-interface/frontend/themes/datamanager/components/page-area.css @@ -95,11 +95,6 @@ font-size: var(--lumo-font-size-s); } -.experiment-details-component vaadin-tabsheet { - height: 100%; - width: 100%; -} - /*Necessary so the disclaimers are aligned to the center of the tab*/ .experiment-details-component .details-content .experimental-groups-container { height: 100%; @@ -382,8 +377,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 { @@ -398,6 +392,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 @@ -548,19 +548,6 @@ white-space: nowrap; } -.sample-details-component .button-and-search-bar { - display: flex; - justify-content: space-between; - gap: var(--lumo-space-s); - margin-bottom: var(--lumo-space-m); -} - -.sample-details-component .button-bar { - gap: var(--lumo-space-s); - display: inline-flex; - align-items: end; -} - .sample-details-component .sample-details-content { display: flex; flex-direction: column; @@ -568,15 +555,10 @@ height: 100%; } -.sample-details-component .sample-tab-content { - height: 100%; - width: 100%; -} - -.sample-details-component .search-bar { - gap: var(--lumo-space-s); +.sample-details-component .sample-count { display: inline-flex; - align-items: end; + padding-bottom: var(--lumo-space-s); + color: var(--lumo-secondary-text-color); } .user-profile-component { diff --git a/user-interface/frontend/themes/datamanager/components/span.css b/user-interface/frontend/themes/datamanager/components/span.css index 992b9e28a..1815eaca0 100644 --- a/user-interface/frontend/themes/datamanager/components/span.css +++ b/user-interface/frontend/themes/datamanager/components/span.css @@ -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); diff --git a/user-interface/src/main/bundles/README.md b/user-interface/src/main/bundles/README.md new file mode 100644 index 000000000..581b703fa --- /dev/null +++ b/user-interface/src/main/bundles/README.md @@ -0,0 +1,32 @@ +This directory is automatically generated by Vaadin and contains the pre-compiled +frontend files/resources for your project (frontend development bundle). + +It should be added to Version Control System and committed, so that other developers +do not have to compile it again. + +Frontend development bundle is automatically updated when needed: +- an npm/pnpm package is added with @NpmPackage or directly into package.json +- CSS, JavaScript or TypeScript files are added with @CssImport, @JsModule or @JavaScript +- Vaadin add-on with front-end customizations is added +- Custom theme imports/assets added into 'theme.json' file +- Exported web component is added. + +If your project development needs a hot deployment of the frontend changes, +you can switch Flow to use Vite development server (default in Vaadin 23.3 and earlier versions): +- set `vaadin.frontend.hotdeploy=true` in `application.properties` +- configure `vaadin-maven-plugin`: +``` + + true + +``` +- configure `jetty-maven-plugin`: +``` + + + true + + +``` + +Read more [about Vaadin development mode](https://vaadin.com/docs/next/configuration/development-mode/#pre-compiled-front-end-bundle-for-faster-start-up). \ No newline at end of file diff --git a/user-interface/src/main/bundles/dev.bundle b/user-interface/src/main/bundles/dev.bundle new file mode 100644 index 000000000..5fc763920 Binary files /dev/null and b/user-interface/src/main/bundles/dev.bundle differ diff --git a/user-interface/src/main/java/life/qbic/datamanager/security/OidcUserDetailsService.java b/user-interface/src/main/java/life/qbic/datamanager/security/OidcUserDetailsService.java index 4f7ee38cd..477c0b128 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/security/OidcUserDetailsService.java +++ b/user-interface/src/main/java/life/qbic/datamanager/security/OidcUserDetailsService.java @@ -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 { diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/oidc/OidcLogo.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/oidc/OidcLogo.java new file mode 100644 index 000000000..c2c775cf5 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/oidc/OidcLogo.java @@ -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())); + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/general/oidc/OidcType.java b/user-interface/src/main/java/life/qbic/datamanager/views/general/oidc/OidcType.java new file mode 100644 index 000000000..e911d85fe --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/general/oidc/OidcType.java @@ -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; + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/login/LoginLayout.java b/user-interface/src/main/java/life/qbic/datamanager/views/login/LoginLayout.java index ad793b818..fdfa61de6 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/login/LoginLayout.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/login/LoginLayout.java @@ -21,14 +21,14 @@ import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import com.vaadin.flow.router.RouterLink; -import com.vaadin.flow.server.AbstractStreamResource; -import com.vaadin.flow.server.StreamResource; import com.vaadin.flow.server.auth.AnonymousAllowed; import com.vaadin.flow.spring.annotation.UIScope; import java.util.List; import java.util.Map; import life.qbic.datamanager.views.AppRoutes; import life.qbic.datamanager.views.AppRoutes.Projects; +import life.qbic.datamanager.views.general.oidc.OidcLogo; +import life.qbic.datamanager.views.general.oidc.OidcType; import life.qbic.datamanager.views.landing.LandingPageLayout; import life.qbic.datamanager.views.notifications.ErrorMessage; import life.qbic.datamanager.views.notifications.InformationMessage; @@ -59,7 +59,6 @@ public class LoginLayout extends VerticalLayout implements HasUrlParameter getClass().getClassLoader().getResourceAsStream(ORCID_LOGO_PATH)); - } - private void styleNotificationLayout() { notificationLayout.setPadding(false); } @@ -238,13 +233,10 @@ public void onEmailConfirmationFailure(String reason) { private static class LoginCard extends Span { private final Span text = new Span(); - private final Image logo = new Image(); - public LoginCard(AbstractStreamResource imageResource, String description, String url) { - logo.addClassName("logo"); + public LoginCard(Image logo, String description, String url) { text.setText(description); text.addClassName("text"); - logo.setSrc(imageResource); add(logo, text); addClassName("login-card"); addClickListener(event -> UI.getCurrent().getPage().open(url, "_self")); diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/access/AddCollaboratorToProjectDialog.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/access/AddCollaboratorToProjectDialog.java index 59e986687..44b527771 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/access/AddCollaboratorToProjectDialog.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/access/AddCollaboratorToProjectDialog.java @@ -1,6 +1,7 @@ package life.qbic.datamanager.views.projects.project.access; import static java.util.Objects.requireNonNull; +import static life.qbic.logging.service.LoggerFactory.logger; import com.vaadin.flow.component.ClickEvent; import com.vaadin.flow.component.Component; @@ -22,9 +23,10 @@ import life.qbic.application.commons.SortOrder; import life.qbic.datamanager.views.account.UserAvatar; import life.qbic.datamanager.views.general.DialogWindow; +import life.qbic.datamanager.views.projects.project.access.ProjectAccessComponent.UserInfoComponent; import life.qbic.identity.api.UserInfo; import life.qbic.identity.api.UserInformationService; -import life.qbic.projectmanagement.application.authorization.acl.ProjectAccessService; +import life.qbic.logging.api.Logger; import life.qbic.projectmanagement.application.authorization.acl.ProjectAccessService.ProjectCollaborator; import life.qbic.projectmanagement.application.authorization.acl.ProjectAccessService.ProjectRole; import life.qbic.projectmanagement.application.authorization.acl.ProjectAccessService.ProjectRoleRecommendationRenderer; @@ -44,6 +46,7 @@ public class AddCollaboratorToProjectDialog extends DialogWindow { @Serial private static final long serialVersionUID = 6582904858073255011L; + private static final Logger log = logger(AddCollaboratorToProjectDialog.class); private final Div projectRoleSelectionSection = new Div(); private final Div personSelectionSection = new Div(); private final RadioButtonGroup projectRoleSelection = new RadioButtonGroup<>(); @@ -51,20 +54,31 @@ public class AddCollaboratorToProjectDialog extends DialogWindow { private final ProjectId projectId; public AddCollaboratorToProjectDialog(UserInformationService userInformationService, - ProjectAccessService projectAccessService, ProjectId projectId, - List alreadyExistingCollaborators) { + ProjectId projectId, + List projectCollaborators) { requireNonNull(userInformationService, "userInformationService must not be null"); - requireNonNull(projectAccessService, "projectAccessService must not be null"); this.projectId = requireNonNull(projectId, "projectId must not be null"); addClassName("add-user-to-project-dialog"); - initPersonSelection(userInformationService, alreadyExistingCollaborators); + initPersonSelection(userInformationService, projectCollaborators); initProjectRoleSelection(); setHeaderTitle("Add Collaborator"); add(personSelectionSection, projectRoleSelectionSection); } + private static Component renderUserInfo(UserInfo userInfo) { + UserAvatar userAvatar = new UserAvatar(); + userAvatar.setUserId(userInfo.id()); + userAvatar.setName(userInfo.platformUserName()); + UserInfoComponent userInfoComponent = new UserInfoComponent(userAvatar, + userInfo.platformUserName(), userInfo.fullName()); + if (userInfo.oidcId() != null && userInfo.oidcIssuer() != null) { + userInfoComponent.setOidc(userInfo.oidcIssuer(), userInfo.oidcId()); + } + return userInfoComponent; + } + private void initPersonSelection(UserInformationService userInformationService, - List alreadyExistingCollaborators) { + List projectCollaborators) { Span title = new Span("Select the person"); title.addClassNames("section-title"); Span description = new Span( @@ -76,14 +90,16 @@ private void initPersonSelection(UserInformationService userInformationService, .collect(Collectors.toCollection(ArrayList::new)); // if no order is provided by the grid order by username sortOrders.add(SortOrder.of("userName").descending()); - List allActiveWithUsername = userInformationService.findAllActive( + List activeUsersWithFilter = userInformationService.queryActiveUsersWithFilter( query.getFilter().orElse(null), query.getOffset(), query.getLimit(), List.copyOf(sortOrders)); // filter for not already - return allActiveWithUsername.stream() - .filter(userInfo -> alreadyExistingCollaborators.stream() - .noneMatch(collaborator -> collaborator.userId().equals(userInfo.id()))); + return activeUsersWithFilter.stream() + .filter(userInfo -> projectCollaborators.stream() + .noneMatch( + projectCollaborator -> projectCollaborator.userId().equals(userInfo.id()))); }); + personSelection.setItemLabelGenerator(UserInfo::platformUserName); personSelection.setRenderer( new ComponentRenderer<>(AddCollaboratorToProjectDialog::renderUserInfo)); personSelection.setRequired(true); @@ -92,20 +108,12 @@ private void initPersonSelection(UserInformationService userInformationService, personSelection.setRenderer(new ComponentRenderer<>( AddCollaboratorToProjectDialog::renderUserInfo )); - personSelection.setItemLabelGenerator(UserInfo::platformUserName); personSelection.addClassName("person-selection"); personSelectionSection.addClassName("person-selection-section"); personSelectionSection.add(title, description, personSelection); } - private static Component renderUserInfo(UserInfo userInfo) { - UserAvatar userAvatar = new UserAvatar(); - userAvatar.setUserId(userInfo.id()); - userAvatar.setName(userInfo.platformUserName()); - return new UserAvatarWithNameComponent(userAvatar, userInfo.platformUserName()); - } - private void initProjectRoleSelection() { Span title = new Span("Assign a Role"); title.addClassNames("section-title"); diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/access/ProjectAccessComponent.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/access/ProjectAccessComponent.java index 1cffcc5c1..1d7968ddc 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/access/ProjectAccessComponent.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/access/ProjectAccessComponent.java @@ -7,10 +7,11 @@ import com.vaadin.flow.component.Component; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.grid.Grid; -import com.vaadin.flow.component.grid.Grid.Column; import com.vaadin.flow.component.grid.Grid.SelectionMode; import com.vaadin.flow.component.grid.GridSortOrder; import com.vaadin.flow.component.grid.editor.Editor; +import com.vaadin.flow.component.html.Anchor; +import com.vaadin.flow.component.html.AnchorTarget; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.select.Select; @@ -20,6 +21,7 @@ import com.vaadin.flow.spring.annotation.SpringComponent; import com.vaadin.flow.spring.annotation.UIScope; import java.io.Serial; +import java.util.Arrays; import java.util.List; import java.util.Objects; import life.qbic.application.commons.ApplicationException; @@ -27,11 +29,12 @@ import life.qbic.datamanager.views.Context; import life.qbic.datamanager.views.account.UserAvatar; import life.qbic.datamanager.views.general.PageArea; +import life.qbic.datamanager.views.general.oidc.OidcLogo; +import life.qbic.datamanager.views.general.oidc.OidcType; import life.qbic.datamanager.views.notifications.ErrorMessage; import life.qbic.datamanager.views.notifications.StyledNotification; import life.qbic.datamanager.views.projects.project.access.AddCollaboratorToProjectDialog.ConfirmEvent; import life.qbic.identity.api.AuthenticationToUserIdTranslator; -import life.qbic.identity.api.UserInfo; import life.qbic.identity.api.UserInformationService; import life.qbic.logging.api.Logger; import life.qbic.projectmanagement.application.authorization.acl.ProjectAccessService; @@ -63,7 +66,8 @@ public class ProjectAccessComponent extends PageArea { private final transient ProjectAccessService projectAccessService; private final transient UserInformationService userInformationService; private final UserPermissions userPermissions; - private final Grid projectCollaborators; + private final Grid projectUserGrid; + private final Div header; private final Span buttonBar; private final AuthenticationToUserIdTranslator authenticationToUserIdTranslator; private Context context; @@ -83,97 +87,127 @@ protected ProjectAccessComponent( this.addClassName("project-access-component"); log.debug("New instance for %s(#%d)".formatted(ProjectAccessComponent.class.getSimpleName(), System.identityHashCode(this))); - - Div header = new Div(); + header = new Div(); header.addClassName("header"); Span titleField = new Span(); titleField.setText("Project Access Management"); titleField.addClassName("title"); - buttonBar = new Span(); - header.add(titleField, buttonBar); + Button addCollaboratorButton = new Button("Add people"); + addCollaboratorButton.addClickListener(event -> openAddCollaboratorDialog()); + buttonBar.add(addCollaboratorButton); + header.add(titleField); add(header); Span userProjectAccessDescription = new Span("Users with access to this project"); - - projectCollaborators = projectCollaboratorGrid(); - add(userProjectAccessDescription, projectCollaborators); + projectUserGrid = createProjectUserGrid(); + add(userProjectAccessDescription, projectUserGrid); } - private boolean isCurrentUser(ProjectAccessService.ProjectCollaborator collaborator) { - var userId = this.authenticationToUserIdTranslator.translateToUserId( - SecurityContextHolder.getContext().getAuthentication()).orElseThrow(); - return Objects.equals(collaborator.userId(), userId); + private static UserInfoComponent renderUserInfo(ProjectUser projectUser) { + UserAvatar userAvatar = new UserAvatar(); + userAvatar.setUserId(projectUser.userId()); + userAvatar.setName(projectUser.userName()); + UserInfoComponent userInfoCellComponent = new UserInfoComponent(userAvatar, + projectUser.userName, projectUser.fullName); + userInfoCellComponent.setOidc(projectUser.oidcIssuer, projectUser.oidc); + return userInfoCellComponent; } - private Button addCollaboratorButton() { - Button button = new Button("Add people"); - button.addClickListener(event -> openAddCollaboratorDialog()); - return button; + public void setContext(Context context) { + if (context.projectId().isEmpty()) { + throw new ApplicationException("no project id in context " + context); + } + this.context = context; + setProjectInformation(); } - private void showControls() { - buttonBar.removeAll(); - buttonBar.add(addCollaboratorButton()); + private void setProjectInformation() { + refreshProjectUserGrid(); + showControls(userPermissions.changeProjectAccess(context.projectId().orElseThrow())); } - private void removeControls() { - buttonBar.removeAll(); + private void refreshProjectUserGrid() { + loadProjectUsers(); + projectUserGrid.setItems(loadProjectUsers()); } - private Grid projectCollaboratorGrid() { + private boolean isCurrentUser(ProjectUser projectUser) { + var userId = this.authenticationToUserIdTranslator.translateToUserId( + SecurityContextHolder.getContext().getAuthentication()).orElseThrow(); + return Objects.equals(projectUser.userId(), userId); + } - Grid grid = new Grid<>(); - Editor editor = grid.getEditor(); - Binder binder = new Binder<>(ProjectCollaborator.class); - editor.setBinder(binder); - var usernameColumn = grid.addComponentColumn( - projectCollaborator -> userInformationService.findById(projectCollaborator.userId()) - .map(ProjectAccessComponent::renderUserInfo) - .orElse(null)) - .setKey("user").setHeader("User").setAutoWidth(true); - Column projectRoleColumn = grid.addColumn( - collaborator -> "Role: " + collaborator.projectRole().label()) - .setKey("projectRole").setHeader("Role").setEditorComponent( - this::renderProjectRoleComponent).setAutoWidth(true); - grid.addComponentColumn(collaborator -> { - //You can't remove or edit your own role - if (isCurrentUser(collaborator)) { - return new Span(); + private void showControls(boolean isVisible) { + boolean containsButtonBar = header.getChildren() + .anyMatch(component -> component.equals(buttonBar)); + if (isVisible) { + if (!containsButtonBar) { + header.add(buttonBar); } - //You can't remove or edit the project owner - if (collaborator.projectRole() == ProjectRole.OWNER) { - return new Span(); - } - //You don't have the rights to change the user - if (!userPermissions.changeProjectAccess(context.projectId().orElseThrow())) { - return new Span(); + } else { + if (containsButtonBar) { + header.remove(buttonBar); } - return changeProjectAccessCell(collaborator); - }).setHeader("Action").setAutoWidth(true); - grid.sort( - List.of(new GridSortOrder<>(usernameColumn, SortDirection.ASCENDING), - new GridSortOrder<>(projectRoleColumn, SortDirection.DESCENDING))); - grid.setSelectionMode(SelectionMode.NONE); - return grid; + } } - private static Component renderUserInfo(UserInfo userInfo) { - UserAvatar userAvatar = new UserAvatar(); - userAvatar.setUserId(userInfo.id()); - userAvatar.setName(userInfo.platformUserName()); - return new UserAvatarWithNameComponent(userAvatar, userInfo.platformUserName()); + private Grid createProjectUserGrid() { + Grid pUserGrid = new Grid<>(ProjectUser.class); + Editor editor = pUserGrid.getEditor(); + Binder binder = new Binder<>(ProjectUser.class); + editor.setBinder(binder); + var projectUserInfoColumn = pUserGrid.addComponentColumn( + ProjectAccessComponent::renderUserInfo) + .setKey("userinfo") + .setHeader("User Info") + .setAutoWidth(true) + .setSortable(true) + .setComparator(ProjectUser::userName) + .setResizable(true); + var projectRoleColumn = pUserGrid.addColumn( + collaborator -> "Role: " + collaborator.projectRole().label()) + .setKey("projectRole").setHeader("Role") + .setEditorComponent( + this::renderProjectRoleComponent) + .setSortable(true) + .setComparator(projectUser -> projectUser.projectRole().label()) + .setResizable(true) + .setAutoWidth(true); + pUserGrid.addComponentColumn(projectUser -> { + //You can't remove or edit your own role + if (isCurrentUser(projectUser)) { + return new Span(); + } + //You can't remove or edit the project owner + if (projectUser.projectRole() == ProjectRole.OWNER) { + return new Span(); + } + //You don't have the rights to change the user + if (!userPermissions.changeProjectAccess(context.projectId().orElseThrow())) { + return new Span(); + } + return changeProjectAccessCell(projectUser); + }) + .setHeader("Action") + .setAutoWidth(true); + pUserGrid.sort( + List.of(new GridSortOrder<>(projectUserInfoColumn, SortDirection.DESCENDING), + new GridSortOrder<>(projectRoleColumn, SortDirection.DESCENDING))); + pUserGrid.setSelectionMode(SelectionMode.NONE); + pUserGrid.setColumnReorderingAllowed(true); + return pUserGrid; } - private Span changeProjectAccessCell(ProjectCollaborator collaborator) { + private Span changeProjectAccessCell(ProjectUser projectUser) { Span changeProjectAccessCell = new Span(); //We want to ensure that even if the frontend components are shown no event is propagated // if the user doesn't have the correct role or tries to remove himself/the project owner Button removeButton = new Button("Remove", clickEvent -> { - if (isCurrentUser(collaborator)) { + if (isCurrentUser(projectUser)) { displayError("Invalid user removal", "You can't remove yourself from a project"); return; } - if (collaborator.projectRole() == ProjectRole.OWNER) { + if (projectUser.projectRole() == ProjectRole.OWNER) { displayError("Invalid user removal", "You can't remove the owner of a project"); return; } @@ -182,16 +216,22 @@ private Span changeProjectAccessCell(ProjectCollaborator collaborator) { "You don't have permission to remove the user from this project"); return; } - removeCollaborator(collaborator); + ProjectUserRemovalConfirmationNotification projectUserRemovalConfirmationNotification = new ProjectUserRemovalConfirmationNotification( + projectUser); + projectUserRemovalConfirmationNotification.addConfirmListener( + event -> removeCollaborator(projectUser)); + projectUserRemovalConfirmationNotification.addCancelListener( + event -> projectUserRemovalConfirmationNotification.close()); + projectUserRemovalConfirmationNotification.open(); }); //We want to ensure that even if the frontend components are shown no event is propagated // if the user doesn't have the correct role or tries to edit himself/the project owner Button editButton = new Button("Edit", clickEvent -> { - if (isCurrentUser(collaborator)) { + if (isCurrentUser(projectUser)) { displayError("Invalid role edit", "You can't change your own project role"); return; } - if (collaborator.projectRole() == ProjectRole.OWNER) { + if (projectUser.projectRole() == ProjectRole.OWNER) { displayError("Invalid role edit", "You can't change the owner of this project"); return; } @@ -200,27 +240,39 @@ private Span changeProjectAccessCell(ProjectCollaborator collaborator) { "You don't have permission to change the role of this collaborator"); return; } - if (projectCollaborators.getEditor().isOpen()) { - projectCollaborators.getEditor().cancel(); - projectCollaborators.getEditor().closeEditor(); + if (projectUserGrid.getEditor().isOpen()) { + projectUserGrid.getEditor().cancel(); + projectUserGrid.getEditor().closeEditor(); return; } - projectCollaborators.getEditor().editItem(collaborator); + projectUserGrid.getEditor().editItem(projectUser); }); changeProjectAccessCell.add(editButton, removeButton); changeProjectAccessCell.addClassName("change-project-access-cell"); return changeProjectAccessCell; } - private void reloadProjectCollaborators(Grid grid, - ProjectAccessService projectAccessService) { - List collaborators = projectAccessService.listCollaborators( + private List loadProjectUsers() { + var projectCollaborators = projectAccessService.listCollaborators( context.projectId().orElseThrow()); - grid.setItems(collaborators); + return projectCollaborators.stream().map(collaborator -> + { + var userInfo = userInformationService.findById(collaborator.userId()).orElseThrow(); + var oidcId = ""; + var oidcIssuer = ""; + if (userInfo.oidcId() != null) { + oidcId = userInfo.oidcId(); + } + if (userInfo.oidcIssuer() != null) { + oidcIssuer = userInfo.oidcIssuer(); + } + return new ProjectUser(collaborator.userId(), userInfo.platformUserName(), + userInfo.fullName(), oidcId, oidcIssuer, collaborator.projectRole()); + }).toList(); } private Component renderProjectRoleComponent( - ProjectAccessService.ProjectCollaborator collaborator) { + ProjectUser projectUser) { String labelPrefix = "Role: "; Select roleSelect = new Select<>(); roleSelect.addClassName("project-role-select"); @@ -245,53 +297,33 @@ private Component renderProjectRoleComponent( return projectRoleDiv; })); - roleSelect.setValue(collaborator.projectRole()); + roleSelect.setValue(projectUser.projectRole()); roleSelect.addValueChangeListener(valueChanged -> { - onProjectRoleSelectionChanged(collaborator, valueChanged); - projectCollaborators.getEditor().save(); - projectCollaborators.getEditor().closeEditor(); - reloadProjectCollaborators(projectCollaborators, projectAccessService); + onProjectRoleSelectionChanged(projectUser, valueChanged); + projectUserGrid.getEditor().save(); + projectUserGrid.getEditor().closeEditor(); + refreshProjectUserGrid(); }); return roleSelect; } - private void removeCollaborator(ProjectAccessService.ProjectCollaborator collaborator) { + private void removeCollaborator(ProjectUser projectUser) { ProjectId projectId = context.projectId().orElseThrow(); - projectAccessService.removeCollaborator(projectId, collaborator.userId()); - reloadProjectCollaborators(projectCollaborators, projectAccessService); + projectAccessService.removeCollaborator(projectId, projectUser.userId()); + refreshProjectUserGrid(); } - private void onProjectRoleSelectionChanged(ProjectAccessService.ProjectCollaborator collaborator, + private void onProjectRoleSelectionChanged(ProjectUser projectUser, ComponentValueChangeEvent, ProjectRole> valueChanged) { - projectAccessService.changeRole(context.projectId().orElseThrow(), collaborator.userId(), + projectAccessService.changeRole(context.projectId().orElseThrow(), projectUser.userId(), valueChanged.getValue()); } - - public void setContext(Context context) { - if (context.projectId().isEmpty()) { - throw new ApplicationException("no project id in context " + context); - } - this.context = context; - onProjectChanged(); - } - - private void onProjectChanged() { - reloadProjectCollaborators(projectCollaborators, projectAccessService); - if (userPermissions.changeProjectAccess(context.projectId().orElseThrow())) { - showControls(); - } else { - removeControls(); - } - } - - private void openAddCollaboratorDialog() { - List alreadyExistingCollaborators = projectAccessService.listCollaborators( + List alreadyExistingCollaborators = projectAccessService.listCollaborators( context.projectId().orElseThrow()); AddCollaboratorToProjectDialog addCollaboratorToProjectDialog = new AddCollaboratorToProjectDialog( - userInformationService, projectAccessService, context.projectId().orElseThrow(), - alreadyExistingCollaborators); + userInformationService, context.projectId().orElseThrow(), alreadyExistingCollaborators); addCollaboratorToProjectDialog.open(); addCollaboratorToProjectDialog.addCancelListener(event -> event.getSource().close()); addCollaboratorToProjectDialog.addConfirmListener(this::onAddCollaboratorConfirmed); @@ -301,7 +333,7 @@ private void onAddCollaboratorConfirmed(ConfirmEvent event) { projectAccessService.addCollaborator(context.projectId().orElseThrow(), event.projectCollaborator() .userId(), event.projectCollaborator().projectRole()); - reloadProjectCollaborators(projectCollaborators, projectAccessService); + refreshProjectUserGrid(); event.getSource().close(); } @@ -311,4 +343,64 @@ private void displayError(String title, String description) { notification.open(); } + /** + * A user in a specific project. + * + * @param userId the collaborating user + * @param userName the unique username of the user + * @param fullName the full name of the user + * @param oidc the oidc of the user + * @param projectRole the role of the user within the project + */ + public record ProjectUser(String userId, String userName, String fullName, String oidc, + String oidcIssuer, ProjectRole projectRole) { + } + + /** + * A component displaying a users avatar, orcid, full name and username + */ + public static class UserInfoComponent extends Div { + + private final Div userInfoContent; + + public UserInfoComponent(UserAvatar userAvatar, String userName, String fullName) { + addClassName("user-info-component"); + userAvatar.addClassName("avatar"); + userInfoContent = new Div(); + userInfoContent.addClassName("user-info"); + add(userAvatar, userInfoContent); + setUserNameAndFullName(userName, fullName); + } + + private void setUserNameAndFullName(String userName, String fullName) { + Span fullNameSpan = new Span(fullName); + Span userNameSpan = new Span(userName); + userNameSpan.addClassName("bold"); + fullNameSpan.addClassName("secondary"); + Span userNameAndFullName = new Span(userNameSpan, fullNameSpan); + userNameAndFullName.addClassName("user-name-and-full-name"); + userInfoContent.add(userNameAndFullName); + } + + protected void setOidc(String oidcIssuer, String oidc) { + if (oidcIssuer.isEmpty() || oidc.isEmpty()) { + return; + } + Arrays.stream(OidcType.values()) + .filter(ot -> ot.getIssuer().equals(oidcIssuer)) + .findFirst() + .ifPresentOrElse(oidcType -> addOidcInfoItem(oidcType, oidc), + () -> log.warn("Unknown oidc Issuer %s".formatted(oidcIssuer))); + } + + private void addOidcInfoItem(OidcType oidcType, String oidc) { + String oidcUrl = String.format(oidcType.getUrl()) + oidc; + Anchor oidcLink = new Anchor(oidcUrl, oidcUrl); + oidcLink.setTarget(AnchorTarget.BLANK); + OidcLogo oidcLogo = new OidcLogo(oidcType); + Span oidcSpan = new Span(oidcLogo, oidcLink); + oidcSpan.addClassName("oidc"); + userInfoContent.add(oidcSpan); + } + } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/access/ProjectUserRemovalConfirmationNotification.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/access/ProjectUserRemovalConfirmationNotification.java new file mode 100644 index 000000000..2b0b86447 --- /dev/null +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/access/ProjectUserRemovalConfirmationNotification.java @@ -0,0 +1,32 @@ +package life.qbic.datamanager.views.projects.project.access; + +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import life.qbic.datamanager.views.notifications.NotificationDialog; +import life.qbic.datamanager.views.projects.project.access.ProjectAccessComponent.ProjectUser; + +/** + * Warns a user that the user will be removed from the project + *

+ * This dialog is to be shown when a user is removed from a project within the + * {@link ProjectAccessComponent} + */ +public class ProjectUserRemovalConfirmationNotification extends NotificationDialog { + + public ProjectUserRemovalConfirmationNotification(ProjectUser projectUser) { + customizeHeader(); + content.add(new Span( + "Are you sure you want to remove the user %s from the project?".formatted( + projectUser.userName()))); + setCancelable(true); + setConfirmText("Confirm"); + } + + private void customizeHeader() { + Icon warningIcon = new Icon(VaadinIcon.WARNING); + warningIcon.setClassName("warning-icon"); + setTitle("Remove user from project"); + setHeaderIcon(warningIcon); + } +} diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementDetailsComponent.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementDetailsComponent.java index 400257d68..4c69f4c39 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementDetailsComponent.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementDetailsComponent.java @@ -198,7 +198,7 @@ private void createNGSMeasurementGrid() { expandSpan.addClickListener(event -> measurementPooledSamplesDialog.open()); return expandSpan; }) - .setHeader("Sample IDs") + .setHeader("Samples") .setAutoWidth(true); ngsMeasurementGrid.addColumn(NGSMeasurement::facility) .setHeader("Facility") @@ -289,7 +289,7 @@ private void createProteomicsGrid() { expandSpan.addClickListener(event -> measurementPooledSamplesDialog.open()); return expandSpan; }) - .setHeader("Sample IDs") + .setHeader("Samples") .setAutoWidth(true); proteomicsMeasurementGrid.addComponentColumn( proteomicsMeasurement -> renderOrganisation(proteomicsMeasurement.organisation())) @@ -525,7 +525,7 @@ private void setPooledProteomicSampleDetails( Grid sampleDetailsGrid = new Grid<>(); sampleDetailsGrid.addColumn( metadata -> retrieveSampleById(metadata.measuredSample()).orElseThrow().label()) - .setHeader("Sample Label") + .setHeader("Sample Name") .setTooltipGenerator( metadata -> retrieveSampleById(metadata.measuredSample()).orElseThrow().label()) .setAutoWidth(true); @@ -558,7 +558,7 @@ private void setPooledNgsSampleDetails( Grid sampleDetailsGrid = new Grid<>(); sampleDetailsGrid.addColumn( metadata -> retrieveSampleById(metadata.measuredSample()).orElseThrow().label()) - .setHeader("Sample Label") + .setHeader("Sample Name") .setTooltipGenerator( metadata -> retrieveSampleById(metadata.measuredSample()).orElseThrow().label()) .setAutoWidth(true); diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementMain.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementMain.java index 872ec09ad..70245f85f 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementMain.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/MeasurementMain.java @@ -536,6 +536,7 @@ private void routeToRawData(ComponentEvent componentEvent) { private void setSelectedMeasurementsInfo(int selectedMeasurements) { String text = "%s measurements are currently selected.".formatted( String.valueOf(selectedMeasurements)); + measurementsSelectedInfoBox.setVisible(selectedMeasurements > 0); measurementsSelectedInfoBox.setText(text); } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/NGSMeasurementContentProvider.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/NGSMeasurementContentProvider.java index b6cee5b56..cf1a7458f 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/NGSMeasurementContentProvider.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/NGSMeasurementContentProvider.java @@ -224,7 +224,7 @@ enum NGSMeasurementColumns { SAMPLEID("QBiC Sample Id", 1, true), SAMPLELABEL( - "Sample label", 2, + "Sample Name", 2, true), POOLGROUP("Sample Pool Group", 3, true), diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/ProteomicsMeasurementContentProvider.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/ProteomicsMeasurementContentProvider.java index 463a41dfd..8f00e1cdf 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/ProteomicsMeasurementContentProvider.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/measurements/download/ProteomicsMeasurementContentProvider.java @@ -53,7 +53,7 @@ private static void formatHeader(Row header, CellStyle readOnlyHeader, CellStyle h2.setCellStyle(readOnlyHeader); var h3 = header.createCell(2); - h3.setCellValue("Sample label"); + h3.setCellValue("Sample Name"); h3.setCellStyle(readOnlyHeader); var h4 = header.createCell(3); diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/BatchDetailsComponent.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/BatchDetailsComponent.java index 3e55aed4a..4de432b0a 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/BatchDetailsComponent.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/BatchDetailsComponent.java @@ -23,7 +23,6 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Collection; -import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.stream.Stream; @@ -32,9 +31,7 @@ import life.qbic.datamanager.ClientDetailsProvider.ClientDetails; import life.qbic.datamanager.views.Context; import life.qbic.datamanager.views.general.PageArea; -import life.qbic.datamanager.views.projects.project.samples.BatchDetailsComponent.BatchPreview.ViewBatchEvent; import life.qbic.projectmanagement.application.batch.BatchInformationService; -import life.qbic.projectmanagement.application.experiment.ExperimentInformationService; import life.qbic.projectmanagement.domain.model.batch.Batch; import life.qbic.projectmanagement.domain.model.batch.BatchId; import life.qbic.projectmanagement.domain.model.experiment.Experiment; @@ -63,24 +60,19 @@ public class BatchDetailsComponent extends PageArea implements Serializable { private final Div content = new Div(); private final Grid batchGrid = new Grid<>(); private final transient BatchInformationService batchInformationService; - private final transient ExperimentInformationService experimentInformationService; - private final Collection batchPreviews = new LinkedHashSet<>(); private final ClientDetailsProvider clientDetailsProvider; private Context context; public BatchDetailsComponent(@Autowired BatchInformationService batchInformationService, - @Autowired ExperimentInformationService experimentInformationService, ClientDetailsProvider clientDetailsProvider) { - Objects.requireNonNull(batchInformationService); - Objects.requireNonNull(experimentInformationService); - Objects.requireNonNull(clientDetailsProvider); + this.batchInformationService = Objects.requireNonNull(batchInformationService, + "BatchInformationService cannot be null"); + this.clientDetailsProvider = Objects.requireNonNull(clientDetailsProvider, + "ClientDetailsProvider cannot be null"); addClassName("batch-details-component"); createTitleAndControls(); createBatchGrid(); add(content); - this.experimentInformationService = experimentInformationService; - this.batchInformationService = batchInformationService; - this.clientDetailsProvider = clientDetailsProvider; } private void createTitleAndControls() { @@ -98,34 +90,40 @@ private void createBatchGrid() { content.addClassName("content"); batchGrid.addColumn(BatchPreview::batchLabel) .setHeader("Name").setSortable(true) - .setTooltipGenerator(BatchPreview::batchLabel).setFlexGrow(1).setAutoWidth(true); + .setTooltipGenerator(BatchPreview::batchLabel) + .setAutoWidth(true) + .setResizable(true); batchGrid.addColumn(new LocalDateTimeRenderer<>( batchPreview -> asClientLocalDateTime(batchPreview.createdOn()), "yyyy-MM-dd")) .setKey("createdOn") .setHeader("Date Created") .setSortable(true) - .setComparator(BatchPreview::createdOn); + .setComparator(BatchPreview::createdOn) + .setAutoWidth(true); batchGrid.addColumn(new LocalDateTimeRenderer<>( batchPreview -> asClientLocalDateTime(batchPreview.lastModified()), "yyyy-MM-dd")) .setKey("lastModified") .setHeader("Date Modified") .setSortable(true) - .setComparator(BatchPreview::lastModified); + .setComparator(BatchPreview::lastModified) + .setAutoWidth(true); batchGrid.addColumn(batchPreview -> batchPreview.samples.size()) .setKey("samples") .setHeader("Samples") - .setSortable(true); - batchGrid.addComponentColumn(this::generateEditorButtons).setAutoWidth(true) - .setHeader("Action"); + .setSortable(true) + .setAutoWidth(true); + batchGrid.addComponentColumn(this::generateEditorButtons) + .setAutoWidth(true) + .setHeader("Action") + .setFrozenToEnd(true); batchGrid.addThemeVariants(GridVariant.LUMO_COMPACT); batchGrid.addClassName("batch-grid"); batchGrid.setAllRowsVisible(true); } public void setContext(Context context) { - batchPreviews.clear(); if (context.experimentId().isEmpty()) { throw new ApplicationException("no experiment id in context " + context); } @@ -133,12 +131,7 @@ public void setContext(Context context) { throw new ApplicationException("no project id in context " + context); } this.context = context; - ExperimentId experimentId = context.experimentId().get(); - Experiment experiment = experimentInformationService.find( - context.projectId().orElseThrow().value(), experimentId).orElseThrow(); - loadBatchesForExperiment(experiment); - batchGrid.setItems(batchPreviews) - .setSortOrder(BatchPreview::lastModified, SortDirection.DESCENDING); + updateBatchGridDataProvider(context.experimentId().get()); } private Span generateEditorButtons(BatchPreview batchPreview) { @@ -149,6 +142,7 @@ private Span generateEditorButtons(BatchPreview batchPreview) { editButton.addThemeVariants(ButtonVariant.LUMO_ICON, ButtonVariant.LUMO_TERTIARY_INLINE); deleteButton.addThemeVariants(ButtonVariant.LUMO_ICON, ButtonVariant.LUMO_TERTIARY_INLINE, ButtonVariant.LUMO_ERROR); + deleteButton.addThemeVariants(ButtonVariant.LUMO_ICON, ButtonVariant.LUMO_TERTIARY_INLINE); deleteButton.addClickListener(e -> fireEvent(new DeleteBatchEvent(this, batchPreview.batchId(), e.isFromClient()))); editButton.addClickListener( @@ -165,23 +159,16 @@ private BatchPreview generatePreviewFromBatch(Batch batch) { batch.lastModified()); } - private void loadBatchesForExperiment(Experiment experiment) { - batchInformationService.retrieveBatchesForExperiment(experiment.experimentId()) + + private void updateBatchGridDataProvider(ExperimentId experimentId) { + List experimentBatches = batchInformationService.retrieveBatchesForExperiment( + experimentId) .map(Collection::stream) .map(batchStream -> batchStream.map(this::generatePreviewFromBatch)) - .map(Stream::toList) - .onValue(batchPreviews::addAll); - } - - /** - * Register a {@link ComponentEventListener} that will get informed with an - * {@link ViewBatchEvent}, as soon as a user wants to view a {@link Batch} - * - * @param batchViewListener a listener on the batch view trigger - */ - public void addBatchViewListener( - ComponentEventListener batchViewListener) { - addListener(ViewBatchEvent.class, batchViewListener); + .map(Stream::toList).getValue(); + batchGrid.setItems(experimentBatches); + batchGrid.getListDataView().setSortOrder(BatchPreview::batchLabel, SortDirection.DESCENDING); + batchGrid.recalculateColumnWidths(); } /** @@ -237,29 +224,6 @@ public record BatchPreview(BatchId batchId, String batchLabel, List sa Objects.requireNonNull(lastModified); } - /** - * View Batch Event - * - *

Indicates that a user wants to view a {@link Batch} - * within the {@link BatchDetailsComponent} of a project

- */ - public static class ViewBatchEvent extends ComponentEvent { - - @Serial - private static final long serialVersionUID = -5108638994476271770L; - - private final BatchPreview batchPreview; - - public ViewBatchEvent(BatchDetailsComponent source, BatchPreview batchPreview, - boolean fromClient) { - super(source, fromClient); - this.batchPreview = batchPreview; - } - - public BatchPreview batchPreview() { - return batchPreview; - } - } } /** diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/SampleDetailsComponent.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/SampleDetailsComponent.java index 16dc2d8e9..2229c0c38 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/SampleDetailsComponent.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/SampleDetailsComponent.java @@ -1,40 +1,23 @@ package life.qbic.datamanager.views.projects.project.samples; -import static life.qbic.logging.service.LoggerFactory.logger; - -import com.vaadin.flow.component.AbstractField.ComponentValueChangeEvent; -import com.vaadin.flow.component.ComponentEvent; -import com.vaadin.flow.component.ComponentEventListener; -import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.grid.Grid; -import com.vaadin.flow.component.grid.GridVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.icon.VaadinIcon; -import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.data.provider.SortDirection; import com.vaadin.flow.data.renderer.ComponentRenderer; -import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.spring.annotation.SpringComponent; import com.vaadin.flow.spring.annotation.UIScope; import java.io.Serial; import java.io.Serializable; -import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import life.qbic.application.commons.ApplicationException; import life.qbic.application.commons.SortOrder; -import life.qbic.datamanager.views.AppRoutes.Projects; import life.qbic.datamanager.views.Context; -import life.qbic.datamanager.views.general.Disclaimer; -import life.qbic.datamanager.views.general.DisclaimerConfirmedEvent; import life.qbic.datamanager.views.general.PageArea; import life.qbic.datamanager.views.general.Tag; -import life.qbic.datamanager.views.general.download.DownloadProvider; -import life.qbic.datamanager.views.projects.project.samples.download.SampleInformationXLSXProvider; import life.qbic.datamanager.views.projects.project.samples.registration.batch.BatchRegistrationDialog; -import life.qbic.logging.api.Logger; -import life.qbic.projectmanagement.application.experiment.ExperimentInformationService; import life.qbic.projectmanagement.application.sample.SampleInformationService; import life.qbic.projectmanagement.application.sample.SamplePreview; import life.qbic.projectmanagement.domain.model.batch.Batch; @@ -59,79 +42,23 @@ public class SampleDetailsComponent extends PageArea implements Serializable { @Serial private static final long serialVersionUID = 2893730975944372088L; - private static final Logger log = logger(SampleDetailsComponent.class); - private final TextField searchField; - private final Disclaimer noGroupsDefinedDisclaimer; - private final Disclaimer noSamplesRegisteredDisclaimer; - private final DownloadProvider metadataDownload; private final Span countSpan; - private final SampleInformationXLSXProvider sampleInformationXLSXProvider; private final Grid sampleGrid; - private final transient ExperimentInformationService experimentInformationService; private final transient SampleInformationService sampleInformationService; private Context context; - public SampleDetailsComponent(@Autowired SampleInformationService sampleInformationService, - @Autowired ExperimentInformationService experimentInformationService) { - this.experimentInformationService = experimentInformationService; - this.sampleInformationService = sampleInformationService; + public SampleDetailsComponent(@Autowired SampleInformationService sampleInformationService) { + this.sampleInformationService = Objects.requireNonNull(sampleInformationService, + "SampleInformationService cannot be null"); addClassName("sample-details-component"); - - this.searchField = createSearchField(); - searchField.addValueChangeListener(valueChangeEvent -> { - }); - searchField.addValueChangeListener(this::onSearchFieldChanged); - Span fieldBar = new Span(); - fieldBar.addClassName("search-bar"); - fieldBar.add(searchField); - - Span buttonBar = new Span(); - buttonBar.addClassName("button-bar"); - Button metadataDownloadButton = new Button("Download Metadata", - event -> onDownloadMetadataClicked()); - buttonBar.add(metadataDownloadButton); - - sampleInformationXLSXProvider = new SampleInformationXLSXProvider(); - metadataDownload = new DownloadProvider(sampleInformationXLSXProvider); - - Div buttonAndFieldBar = new Div(); - buttonAndFieldBar.addClassName("button-and-search-bar"); - buttonAndFieldBar.add(fieldBar, buttonBar); - - Span title = new Span("Samples"); - title.addClassName("title"); - - addComponentAsFirst(title); - - Div experimentTabContent = new Div(); - experimentTabContent.addClassName("sample-tab-content"); - sampleGrid = createSampleGrid(); - - noGroupsDefinedDisclaimer = createNoGroupsDefinedDisclaimer(); - noGroupsDefinedDisclaimer.setVisible(false); - - noSamplesRegisteredDisclaimer = createNoSamplesRegisteredDisclaimer(); - noSamplesRegisteredDisclaimer.setVisible(false); - - experimentTabContent.add(sampleGrid, noGroupsDefinedDisclaimer, noSamplesRegisteredDisclaimer); - Div content = new Div(); content.addClassName("sample-details-content"); - countSpan = new Span(); - - content.add(buttonAndFieldBar, metadataDownload, countSpan, experimentTabContent); + countSpan.addClassName("sample-count"); + setSampleCount(0); + content.add(countSpan, sampleGrid); add(content); - - } - - private static TextField createSearchField() { - TextField textField = new TextField(); - textField.setPlaceholder("Search"); - textField.setPrefixComponent(VaadinIcon.SEARCH.create()); - textField.setValueChangeMode(ValueChangeMode.EAGER); - return textField; } private static ComponentRenderer createConditionRenderer() { @@ -162,46 +89,74 @@ private static Div createTagCollection() { return tagCollection; } - private static boolean noExperimentGroupsInExperiment(Experiment experiment) { - return experiment.getExperimentalGroups().isEmpty(); - } - private static Grid createSampleGrid() { Grid sampleGrid = new Grid<>(SamplePreview.class); - sampleGrid.addColumn(SamplePreview::sampleCode).setHeader("Sample ID") - .setSortProperty("sampleCode").setAutoWidth(true).setFlexGrow(0) - .setTooltipGenerator(SamplePreview::sampleCode); - sampleGrid.addColumn(SamplePreview::sampleLabel).setHeader("Sample Label") - .setSortProperty("sampleLabel").setTooltipGenerator(SamplePreview::sampleLabel); - sampleGrid.addColumn(SamplePreview::organismId).setHeader("Organism ID") - .setSortProperty("organismId").setTooltipGenerator(SamplePreview::organismId); - sampleGrid.addColumn(SamplePreview::batchLabel).setHeader("Batch") - .setSortProperty("batchLabel").setTooltipGenerator(SamplePreview::batchLabel); - sampleGrid.addColumn(createConditionRenderer()).setHeader("Condition") - .setSortProperty("experimentalGroup").setAutoWidth(true).setFlexGrow(0); - sampleGrid.addColumn(preview -> preview.species().getLabel()).setHeader("Species") + sampleGrid.addColumn(SamplePreview::sampleCode) + .setHeader("Sample ID") + .setSortProperty("sampleCode") + .setAutoWidth(true) + .setFlexGrow(0) + .setTooltipGenerator(SamplePreview::sampleCode) + .setFrozen(true); + sampleGrid.addColumn(SamplePreview::sampleLabel) + .setHeader("Sample Name") + .setSortProperty("sampleName") + .setTooltipGenerator(SamplePreview::sampleLabel) + .setAutoWidth(true) + .setResizable(true); + sampleGrid.addColumn(SamplePreview::organismId) + .setHeader("Organism ID") + .setSortProperty("organismId") + .setTooltipGenerator(SamplePreview::organismId) + .setAutoWidth(true) + .setResizable(true); + sampleGrid.addColumn(SamplePreview::batchLabel) + .setHeader("Batch") + .setSortProperty("batchLabel") + .setTooltipGenerator(SamplePreview::batchLabel) + .setAutoWidth(true) + .setResizable(true); + sampleGrid.addColumn(createConditionRenderer()) + .setHeader("Condition") + .setSortProperty("experimentalGroup") + .setAutoWidth(true) + .setResizable(true); + sampleGrid.addColumn(preview -> preview.species().getLabel()) + .setHeader("Species") .setSortProperty("species") - .setTooltipGenerator(preview -> preview.species().formatted()); - sampleGrid.addColumn(preview -> preview.specimen().getLabel()).setHeader("Specimen") - .setSortProperty("specimen").setTooltipGenerator(preview -> preview.specimen().formatted()); - sampleGrid.addColumn(preview -> preview.analyte().getLabel()).setHeader("Analyte") + .setTooltipGenerator(preview -> preview.species().formatted()) + .setAutoWidth(true) + .setResizable(true); + sampleGrid.addColumn(preview -> preview.specimen().getLabel()) + .setHeader("Specimen") + .setSortProperty("specimen") + .setTooltipGenerator(preview -> preview.specimen().formatted()) + .setAutoWidth(true); + sampleGrid.addColumn(preview -> preview.analyte().getLabel()) + .setHeader("Analyte") .setSortProperty("analyte") - .setTooltipGenerator(preview -> preview.analyte().formatted()); - sampleGrid.addColumn(SamplePreview::analysisMethod).setHeader("Analysis to Perform") - .setSortProperty("analysisMethod").setTooltipGenerator(SamplePreview::analysisMethod); - sampleGrid.addColumn(SamplePreview::comment).setHeader("Comment").setSortProperty("comment") - .setTooltipGenerator(SamplePreview::comment); + .setTooltipGenerator(preview -> preview.analyte().formatted()) + .setAutoWidth(true) + .setResizable(true); + sampleGrid.addColumn(SamplePreview::analysisMethod) + .setHeader("Analysis to Perform") + .setSortProperty("analysisMethod") + .setTooltipGenerator(SamplePreview::analysisMethod) + .setAutoWidth(true) + .setResizable(true); + sampleGrid.addColumn(SamplePreview::comment) + .setHeader("Comment") + .setSortProperty("comment") + .setTooltipGenerator(SamplePreview::comment) + .setAutoWidth(true) + .setResizable(true); sampleGrid.addClassName("sample-grid"); - sampleGrid.addThemeVariants(GridVariant.LUMO_WRAP_CELL_CONTENT); + sampleGrid.setColumnReorderingAllowed(true); return sampleGrid; } - private void onDownloadMetadataClicked() { - metadataDownload.trigger(); - } - - private void onSearchFieldChanged(ComponentValueChangeEvent valueChangeEvent) { - updateSampleGridDataProvider(context.experimentId().orElseThrow(), valueChangeEvent.getValue()); + public void onSearchFieldValueChanged(String searchValue) { + updateSampleGridDataProvider(context.experimentId().orElseThrow(), searchValue); } private void updateSampleGridDataProvider(ExperimentId experimentId, String filter) { @@ -216,6 +171,7 @@ private void updateSampleGridDataProvider(ExperimentId experimentId, String filt }); sampleGrid.getLazyDataView().addItemCountChangeListener( countChangeEvent -> setSampleCount((int) sampleGrid.getLazyDataView().getItems().count())); + sampleGrid.recalculateColumnWidths(); } /** @@ -231,110 +187,11 @@ public void setContext(Context context) { throw new ApplicationException("no project id in context " + context); } this.context = context; - ExperimentId experimentId = context.experimentId().get(); - setExperiment( - experimentInformationService.find(context.projectId().orElseThrow().value(), experimentId) - .orElseThrow()); - - // we also update the data provider with any samples of this experiment - List samples = sampleInformationService.retrieveSamplePreviewsForExperiment( - experimentId); - sampleInformationXLSXProvider.setSamples(samples); - } - - /** - * Adds the provided listener - * - * @param batchRegistrationListener listener notified if the user intends to create a batch - */ - public void addCreateBatchListener( - ComponentEventListener batchRegistrationListener) { - addListener(RegisterBatchClicked.class, batchRegistrationListener); - } - - private void routeToExperimentalGroupCreation(ComponentEvent componentEvent, - String experimentId) { - if (componentEvent.isFromClient()) { - String routeToExperimentPage = String.format(Projects.EXPERIMENT, - context.projectId().orElseThrow().value(), - experimentId); - log.debug(String.format( - "Rerouting to experiment page for experiment %s of project %s: %s", - experimentId, context.projectId().orElseThrow().value(), routeToExperimentPage)); - componentEvent.getSource().getUI().ifPresent(ui -> ui.navigate(routeToExperimentPage)); - } - } - - private void setExperiment(Experiment experiment) { - setSampleCount(0); - - if (noExperimentGroupsInExperiment(experiment)) { - sampleGrid.setVisible(false); - noSamplesRegisteredDisclaimer.setVisible(false); - noGroupsDefinedDisclaimer.setVisible(true); - return; - } - if (noSamplesRegisteredInExperiment(experiment)) { - sampleGrid.setVisible(false); - noSamplesRegisteredDisclaimer.setVisible(true); - noGroupsDefinedDisclaimer.setVisible(false); - return; - } - updateSampleGridDataProvider(context.experimentId().orElseThrow(), searchField.getValue()); - - sampleGrid.setVisible(true); - noSamplesRegisteredDisclaimer.setVisible(false); - noGroupsDefinedDisclaimer.setVisible(false); + updateSampleGridDataProvider(this.context.experimentId().orElseThrow(), ""); } private void setSampleCount(int i) { - countSpan.setText(i+" samples"); - } - - private void onNoGroupsDefinedClicked(DisclaimerConfirmedEvent event) { - routeToExperimentalGroupCreation(event, context.experimentId().orElseThrow().value()); + countSpan.setText(i + " samples"); } - private boolean noSamplesRegisteredInExperiment(Experiment experiment) { - return sampleInformationService.retrieveSamplesForExperiment(experiment.experimentId()) - .map(Collection::isEmpty) - .onError(error -> { - throw new ApplicationException("Unexpected response code : " + error); - }) - .getValue(); - } - - private Disclaimer createNoSamplesRegisteredDisclaimer() { - Disclaimer noSamplesDefinedCard = Disclaimer.createWithTitle( - "Manage your samples in one place", - "Start your project by registering the first sample batch", "Register batch"); - noSamplesDefinedCard.addDisclaimerConfirmedListener( - event -> fireEvent(new RegisterBatchClicked(this, event.isFromClient()))); - return noSamplesDefinedCard; - } - - private Disclaimer createNoGroupsDefinedDisclaimer() { - Disclaimer noGroupsDefindedDisclaimer = Disclaimer.createWithTitle( - "Design your experiment first", - "Start the sample registration process by defining experimental groups", - "Add groups"); - noGroupsDefindedDisclaimer.addDisclaimerConfirmedListener(this::onNoGroupsDefinedClicked); - return noGroupsDefindedDisclaimer; - } - - /** - * Create Batch Event - * - *

Indicates that a user wants to create a {@link Batch} - * within the {@link SampleDetailsComponent} of a project

- */ - public static class RegisterBatchClicked extends ComponentEvent { - - @Serial - private static final long serialVersionUID = 5351296685318048598L; - - public RegisterBatchClicked(SampleDetailsComponent source, boolean fromClient) { - super(source, fromClient); - } - } } diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/SampleInformationMain.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/SampleInformationMain.java index 18ffa85da..d7676d968 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/SampleInformationMain.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/SampleInformationMain.java @@ -1,8 +1,12 @@ package life.qbic.datamanager.views.projects.project.samples; -import com.vaadin.flow.component.Component; import com.vaadin.flow.component.ComponentEvent; -import com.vaadin.flow.component.confirmdialog.ConfirmDialog; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.BeforeEnterObserver; import com.vaadin.flow.router.Route; @@ -15,17 +19,20 @@ import java.util.Comparator; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; import life.qbic.application.commons.ApplicationException; +import life.qbic.datamanager.views.AppRoutes.Projects; import life.qbic.datamanager.views.Context; +import life.qbic.datamanager.views.general.Disclaimer; +import life.qbic.datamanager.views.general.DisclaimerConfirmedEvent; import life.qbic.datamanager.views.general.Main; +import life.qbic.datamanager.views.general.download.DownloadProvider; import life.qbic.datamanager.views.notifications.ErrorMessage; import life.qbic.datamanager.views.notifications.StyledNotification; import life.qbic.datamanager.views.notifications.SuccessMessage; import life.qbic.datamanager.views.projects.project.experiments.ExperimentMainLayout; -import life.qbic.datamanager.views.projects.project.samples.BatchDetailsComponent.BatchPreview.ViewBatchEvent; import life.qbic.datamanager.views.projects.project.samples.BatchDetailsComponent.DeleteBatchEvent; import life.qbic.datamanager.views.projects.project.samples.BatchDetailsComponent.EditBatchEvent; +import life.qbic.datamanager.views.projects.project.samples.download.SampleInformationXLSXProvider; import life.qbic.datamanager.views.projects.project.samples.registration.batch.BatchRegistrationDialog; import life.qbic.datamanager.views.projects.project.samples.registration.batch.BatchRegistrationDialog.ConfirmEvent; import life.qbic.datamanager.views.projects.project.samples.registration.batch.EditBatchDialog; @@ -34,12 +41,12 @@ import life.qbic.logging.api.Logger; import life.qbic.logging.service.LoggerFactory; import life.qbic.projectmanagement.application.DeletionService; -import life.qbic.projectmanagement.application.ProjectInformationService; import life.qbic.projectmanagement.application.batch.BatchRegistrationService; import life.qbic.projectmanagement.application.batch.SampleUpdateRequest; import life.qbic.projectmanagement.application.batch.SampleUpdateRequest.SampleInformation; import life.qbic.projectmanagement.application.experiment.ExperimentInformationService; import life.qbic.projectmanagement.application.sample.SampleInformationService; +import life.qbic.projectmanagement.application.sample.SamplePreview; import life.qbic.projectmanagement.application.sample.SampleRegistrationService; import life.qbic.projectmanagement.domain.model.batch.BatchId; import life.qbic.projectmanagement.domain.model.experiment.Experiment; @@ -72,7 +79,6 @@ public class SampleInformationMain extends Main implements BeforeEnterObserver { @Serial private static final long serialVersionUID = 3778218989387044758L; private static final Logger log = LoggerFactory.logger(SampleInformationMain.class); - private final transient ProjectInformationService projectInformationService; private final transient ExperimentInformationService experimentInformationService; private final transient BatchRegistrationService batchRegistrationService; private final transient SampleRegistrationService sampleRegistrationService; @@ -80,39 +86,57 @@ public class SampleInformationMain extends Main implements BeforeEnterObserver { private final transient DeletionService deletionService; private final transient SampleDetailsComponent sampleDetailsComponent; private final BatchDetailsComponent batchDetailsComponent; + + private final DownloadProvider metadataDownload; + private final SampleInformationXLSXProvider sampleInformationXLSXProvider; + + private final Div content = new Div(); + private final TextField searchField = new TextField(); + private final Disclaimer noGroupsDefinedDisclaimer; + private final Disclaimer noSamplesRegisteredDisclaimer; private transient Context context; - public SampleInformationMain(@Autowired ProjectInformationService projectInformationService, - @Autowired ExperimentInformationService experimentInformationService, + public SampleInformationMain(@Autowired ExperimentInformationService experimentInformationService, @Autowired BatchRegistrationService batchRegistrationService, @Autowired DeletionService deletionService, @Autowired SampleRegistrationService sampleRegistrationService, @Autowired SampleInformationService sampleInformationService, @Autowired SampleDetailsComponent sampleDetailsComponent, @Autowired BatchDetailsComponent batchDetailsComponent) { - Objects.requireNonNull(projectInformationService); - Objects.requireNonNull(experimentInformationService); - Objects.requireNonNull(batchRegistrationService); - Objects.requireNonNull(sampleRegistrationService); - Objects.requireNonNull(sampleInformationService); - Objects.requireNonNull(deletionService); - Objects.requireNonNull(sampleDetailsComponent); - Objects.requireNonNull(batchDetailsComponent); - this.projectInformationService = projectInformationService; - this.experimentInformationService = experimentInformationService; - this.batchRegistrationService = batchRegistrationService; - this.sampleRegistrationService = sampleRegistrationService; - this.sampleInformationService = sampleInformationService; - this.deletionService = deletionService; - this.sampleDetailsComponent = sampleDetailsComponent; - this.batchDetailsComponent = batchDetailsComponent; - addClassName("sample"); - reloadOnBatchRegistration(); - sampleDetailsComponent.addCreateBatchListener(event -> onRegisterBatchClicked()); + this.experimentInformationService = Objects.requireNonNull(experimentInformationService, + "ExperimentInformationService cannot be null"); + this.batchRegistrationService = Objects.requireNonNull(batchRegistrationService, + "BatchRegistrationService cannot be null"); + this.sampleRegistrationService = Objects.requireNonNull(sampleRegistrationService, + "SampleRegistrationService cannot be null"); + this.sampleInformationService = Objects.requireNonNull(sampleInformationService, + "SampleInformationService cannot be null"); + this.deletionService = Objects.requireNonNull(deletionService, + "DeletionService cannot be null"); + this.sampleDetailsComponent = Objects.requireNonNull(sampleDetailsComponent, + "SampleDetailsComponent cannot be null"); + this.batchDetailsComponent = Objects.requireNonNull(batchDetailsComponent, + "BatchDetailsComponent cannot be null"); + + noGroupsDefinedDisclaimer = createNoGroupsDefinedDisclaimer(); + noGroupsDefinedDisclaimer.setVisible(false); + + noSamplesRegisteredDisclaimer = createNoSamplesRegisteredDisclaimer(); + noSamplesRegisteredDisclaimer.setVisible(false); + + sampleInformationXLSXProvider = new SampleInformationXLSXProvider(); + metadataDownload = new DownloadProvider(sampleInformationXLSXProvider); + + add(noGroupsDefinedDisclaimer, noSamplesRegisteredDisclaimer); + initContent(); + add(sampleDetailsComponent, batchDetailsComponent); + add(metadataDownload); + batchDetailsComponent.addBatchCreationListener(ignored -> onRegisterBatchClicked()); batchDetailsComponent.addBatchDeletionListener(this::onDeleteBatchClicked); batchDetailsComponent.addBatchEditListener(this::onEditBatchClicked); - batchDetailsComponent.addBatchViewListener(this::onViewBatchClicked); + + addClassName("sample"); log.debug(String.format( "New instance for %s(#%s) created with %s(#%s) and %s(#%s)", this.getClass().getSimpleName(), System.identityHashCode(this), @@ -122,40 +146,50 @@ public SampleInformationMain(@Autowired ProjectInformationService projectInforma System.identityHashCode(sampleDetailsComponent))); } - /** - * Provides the {@link Context} to the components within this page - *

- * This method serves as an entry point providing the necessary {@link Context} to the components - * within this cage - * - * @param context Context containing the projectId of the selected project - */ - public void setContext(Context context) { - this.context = context; - ProjectId projectId = context.projectId().orElseThrow(); - batchDetailsComponent.setContext(context); - projectInformationService.find(projectId) - .ifPresentOrElse( - project -> { - sampleDetailsComponent.setContext(context); - displayComponentInContent(batchDetailsComponent); - displayComponentInContent(sampleDetailsComponent); - }, this::displayProjectNotFound); + private static boolean noExperimentGroupsInExperiment(Experiment experiment) { + return experiment.getExperimentalGroups().isEmpty(); } - private boolean isComponentInContent(Component component) { - return this.getChildren().collect(Collectors.toSet()).contains(component); + private void initContent() { + Span titleField = new Span(); + titleField.setText("Register sample batch"); + titleField.addClassNames("title"); + content.add(titleField); + initSearchFieldAndButtonBar(); + add(content); + content.addClassName("sample-main-content"); } - private void displayComponentInContent(Component component) { - if (!isComponentInContent(component)) { - this.add(component); - } + private void initSearchFieldAndButtonBar() { + searchField.setPlaceholder("Search"); + searchField.setClearButtonVisible(true); + searchField.setSuffixComponent(VaadinIcon.SEARCH.create()); + searchField.addClassNames("search-field"); + searchField.setValueChangeMode(ValueChangeMode.LAZY); + searchField.addValueChangeListener( + event -> sampleDetailsComponent.onSearchFieldValueChanged((event.getValue()))); + Button metadataDownloadButton = new Button("Download Sample Metadata", + event -> downloadSampleMetadata()); + Span buttonBar = new Span(metadataDownloadButton); + buttonBar.addClassName("button-bar"); + Span buttonsAndSearch = new Span(searchField, buttonBar); + buttonsAndSearch.addClassName("buttonAndField"); + content.add(buttonsAndSearch); } - private void reloadOnBatchRegistration() { - sampleDetailsComponent.addCreateBatchListener( - event -> displayComponentInContent(sampleDetailsComponent)); + private void downloadSampleMetadata() { + List samplePreviews = sampleInformationService.retrieveSamplePreviewsForExperiment( + context.experimentId() + .orElseThrow()); + + Comparator natOrder = Comparator.naturalOrder(); + + var result = samplePreviews.stream() + // sort by measurement codes first, then by sample codes + .sorted(Comparator.comparing(SamplePreview::sampleCode, natOrder) + .thenComparing(SamplePreview::sampleLabel, natOrder)).toList(); + sampleInformationXLSXProvider.setSamples(result); + metadataDownload.trigger(); } private void onRegisterBatchClicked() { @@ -175,11 +209,6 @@ private void onRegisterBatchClicked() { dialog.open(); } - private void reload() { - setContext(context); - } - - private void registerBatch(ConfirmEvent confirmEvent) { String batchLabel = confirmEvent.getData().batchName(); List samples = confirmEvent.getData().samples(); @@ -196,7 +225,7 @@ private void registerBatch(ConfirmEvent confirmEvent) { .onValue(ignored -> fireEvent(new BatchRegisteredEvent(this, false))) .onValue(ignored -> confirmEvent.getSource().close()) .onValue(batchId -> displayRegistrationSuccess()) - .onValue(ignored -> reload()); + .onValue(ignored -> setBatchAndSampleInformation()); } private List generateSampleRequestsFromSampleInfo(BatchId batchId, @@ -225,6 +254,43 @@ private SampleUpdateRequest generateSampleUpdateRequestFromSampleInfo( sampleInfo.getCustomerComment())); } + private Disclaimer createNoSamplesRegisteredDisclaimer() { + Disclaimer noSamplesDefinedCard = Disclaimer.createWithTitle( + "Manage your samples in one place", + "Start your project by registering the first sample batch", "Register batch"); + noSamplesDefinedCard.addClassName("no-samples-registered-disclaimer"); + noSamplesDefinedCard.addDisclaimerConfirmedListener( + event -> onRegisterBatchClicked()); + return noSamplesDefinedCard; + } + + private Disclaimer createNoGroupsDefinedDisclaimer() { + Disclaimer noGroupsDefindedDisclaimer = Disclaimer.createWithTitle( + "Design your experiment first", + "Start the sample registration process by defining experimental groups", + "Add groups"); + noGroupsDefindedDisclaimer.addClassName("no-experimental-groups-registered-disclaimer"); + noGroupsDefindedDisclaimer.addDisclaimerConfirmedListener(this::onNoGroupsDefinedClicked); + return noGroupsDefindedDisclaimer; + } + + private void onNoGroupsDefinedClicked(DisclaimerConfirmedEvent event) { + routeToExperimentalGroupCreation(event, context.experimentId().orElseThrow().value()); + } + + private void routeToExperimentalGroupCreation(ComponentEvent componentEvent, + String experimentId) { + if (componentEvent.isFromClient()) { + String routeToExperimentPage = String.format(Projects.EXPERIMENT, + context.projectId().orElseThrow().value(), + experimentId); + log.debug(String.format( + "Rerouting to experiment page for experiment %s of project %s: %s", + experimentId, context.projectId().orElseThrow().value(), routeToExperimentPage)); + componentEvent.getSource().getUI().ifPresent(ui -> ui.navigate(routeToExperimentPage)); + } + } + private void displayUpdateSuccess() { SuccessMessage successMessage = new SuccessMessage("Batch update succeeded.", ""); StyledNotification notification = new StyledNotification(successMessage); @@ -237,7 +303,6 @@ private void displayDeletionSuccess() { notification.open(); } - private void displayRegistrationSuccess() { SuccessMessage successMessage = new SuccessMessage("Batch registration succeeded.", ""); StyledNotification notification = new StyledNotification(successMessage); @@ -250,13 +315,6 @@ private void displayRegistrationFailure() { notification.open(); } - private void onViewBatchClicked(ViewBatchEvent viewBatchEvent) { - ConfirmDialog confirmDialog = new ConfirmDialog(); - confirmDialog.setText(("This is where I'd show all of my Samples")); - confirmDialog.open(); - confirmDialog.addConfirmListener(event -> confirmDialog.close()); - } - private void onEditBatchClicked(EditBatchEvent editBatchEvent) { Experiment experiment = context.experimentId() .flatMap( @@ -313,14 +371,14 @@ private void editBatch(EditBatchDialog.ConfirmEvent confirmEvent) { deletedSamples, context.projectId().orElseThrow()); result.onValue(ignored -> confirmEvent.getSource().close()); result.onValue(batchId -> displayUpdateSuccess()); - result.onValue(ignored -> reload()); + result.onValue(ignored -> setBatchAndSampleInformation()); } private void deleteBatch(DeleteBatchEvent deleteBatchEvent) { deletionService.deleteBatch(context.projectId().orElseThrow(), deleteBatchEvent.batchId()); displayDeletionSuccess(); - reload(); + setBatchAndSampleInformation(); } private void onDeleteBatchClicked(DeleteBatchEvent deleteBatchEvent) { @@ -334,14 +392,6 @@ private void onDeleteBatchClicked(DeleteBatchEvent deleteBatchEvent) { event -> batchDeletionConfirmationNotification.close()); } - private void displayProjectNotFound() { - this.removeAll(); - ErrorMessage errorMessage = new ErrorMessage("Project not found", - "Please try to reload the page"); - StyledNotification notification = new StyledNotification(errorMessage); - notification.open(); - } - /** * Callback executed before navigation to attaching Component chain is made. * @@ -363,7 +413,66 @@ public void beforeEnter(BeforeEnterEvent event) { } ExperimentId parsedExperimentId = ExperimentId.parse(experimentId); this.context = context.with(parsedExperimentId); - setContext(context); + setBatchAndSampleInformation(); + } + + private void setBatchAndSampleInformation() { + var experiment = experimentInformationService.find(context.projectId().orElseThrow().value(), + context.experimentId() + .orElseThrow()).orElseThrow(); + if (noExperimentGroupsInExperiment(experiment)) { + showRegisterGroupsDisclaimer(); + return; + } + if (noSamplesRegisteredInExperiment(experiment)) { + showRegisterBatchDisclaimer(); + } else { + reloadBatchInformation(); + reloadSampleInformation(); + showBatchAndSampleInformation(); + } + } + + private boolean noSamplesRegisteredInExperiment(Experiment experiment) { + return sampleInformationService.retrieveSamplesForExperiment(experiment.experimentId()) + .map(Collection::isEmpty) + .onError(error -> { + throw new ApplicationException("Unexpected response code : " + error); + }) + .getValue(); + } + + private void showRegisterGroupsDisclaimer() { + content.setVisible(false); + sampleDetailsComponent.setVisible(false); + batchDetailsComponent.setVisible(false); + noSamplesRegisteredDisclaimer.setVisible(false); + noGroupsDefinedDisclaimer.setVisible(true); + } + + private void showRegisterBatchDisclaimer() { + content.setVisible(false); + sampleDetailsComponent.setVisible(false); + batchDetailsComponent.setVisible(false); + noGroupsDefinedDisclaimer.setVisible(false); + noSamplesRegisteredDisclaimer.setVisible(true); + } + + private void showBatchAndSampleInformation() { + noSamplesRegisteredDisclaimer.setVisible(false); + noGroupsDefinedDisclaimer.setVisible(false); + content.setVisible(true); + sampleDetailsComponent.setVisible(true); + batchDetailsComponent.setVisible(true); + searchField.setValue(""); + } + + private void reloadBatchInformation() { + batchDetailsComponent.setContext(context); + } + + private void reloadSampleInformation() { + sampleDetailsComponent.setContext(context); } public static class BatchRegisteredEvent extends ComponentEvent { diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/SampleMetadataContentProvider.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/SampleMetadataContentProvider.java index f8158dcbd..ed7e8f886 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/SampleMetadataContentProvider.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/SampleMetadataContentProvider.java @@ -31,7 +31,7 @@ public byte[] getContent() { } TSVBuilder tsvBuilder = new TSVBuilder<>(samples); tsvBuilder.addColumn("Sample ID", SamplePreview::sampleCode); - tsvBuilder.addColumn("Label", SamplePreview::sampleLabel); + tsvBuilder.addColumn("Sample Name", SamplePreview::sampleLabel); tsvBuilder.addColumn("Organism ID", SamplePreview::organismId); tsvBuilder.addColumn("Batch", SamplePreview::batchLabel); tsvBuilder.addColumn("Species", sample -> sample.species().getLabel()); diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/download/SampleInformationXLSXProvider.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/download/SampleInformationXLSXProvider.java index 308737bc5..10d4efb3d 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/download/SampleInformationXLSXProvider.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/download/SampleInformationXLSXProvider.java @@ -161,7 +161,7 @@ public String getFileName() { enum SamplePreviewColumn { SAMPLE_ID("Sample ID", 0), - LABEL("Label", 1), + LABEL("Sample Name", 1), ORGANISM_ID("Organism ID", 2), BATCH("Batch", 3), SPECIES("Species", 4), diff --git a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleBatchInformationSpreadsheet.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleBatchInformationSpreadsheet.java index 41f10c846..f1d221d5b 100644 --- a/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleBatchInformationSpreadsheet.java +++ b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleBatchInformationSpreadsheet.java @@ -54,7 +54,7 @@ public SampleBatchInformationSpreadsheet(List experimentalGro .selectFrom(sortedAnalysisMethods, identity(), getAnalysisMethodItemRenderer()) .setRequired(); - addColumn("Sample label", SampleInfo::getSampleLabel, SampleInfo::setSampleLabel) + addColumn("Sample Name", SampleInfo::getSampleLabel, SampleInfo::setSampleLabel) .requireDistinctValues() .setRequired(); diff --git a/user-interface/src/main/resources/impressum/DataPrivacyAgreement.html b/user-interface/src/main/resources/impressum/DataPrivacyAgreement.html index 1c4306d2e..110d1cd91 100644 --- a/user-interface/src/main/resources/impressum/DataPrivacyAgreement.html +++ b/user-interface/src/main/resources/impressum/DataPrivacyAgreement.html @@ -9,7 +9,7 @@

Preamble

the context of providing our application.

The terms used are not gender-specific.

-

Last Update: 5. June 2024

+

Last Update: 6. August 2024

Registration, Login and User Account pursue our claims or there is a legal obligation to do so.

Users may be informed by e-mail of information relevant to their user account, such as technical changes.

+

The login information (username, full name and orcid) is accessible to other users within the + data manager platform for the purpose of project collaboration.

  • Processed data types: Inventory data (For example, the full name, residential address, contact information, customer number, etc.); Contact data (e.g. postal diff --git a/user-interface/src/main/resources/templates/ngs_measurement_registration_sheet.xlsx b/user-interface/src/main/resources/templates/ngs_measurement_registration_sheet.xlsx index cf66ede87..24bb90dfe 100644 Binary files a/user-interface/src/main/resources/templates/ngs_measurement_registration_sheet.xlsx and b/user-interface/src/main/resources/templates/ngs_measurement_registration_sheet.xlsx differ diff --git a/user-interface/src/main/resources/templates/proteomics_measurement_registration_sheet.xlsx b/user-interface/src/main/resources/templates/proteomics_measurement_registration_sheet.xlsx index 32915b759..fa8406e3b 100644 Binary files a/user-interface/src/main/resources/templates/proteomics_measurement_registration_sheet.xlsx and b/user-interface/src/main/resources/templates/proteomics_measurement_registration_sheet.xlsx differ