From ad9eebdf5c3aebabf186ac699951dbb9a4f052a4 Mon Sep 17 00:00:00 2001 From: "Sven F." <9976560+sven1103@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:51:53 +0200 Subject: [PATCH] Refactor SamplePage UI to current Prototype (#769) * Remove batch view button from batchregistration component * Fix sample grid layout * WIP in progress * Update column width of batch component and remove unnecessary logic in BatchDetailsComponent * Update column width of sample grid and remove unnecessary logic in SampleDetailsComponent * Allow user to filter via organismId while searching for samples in SampleInformationMain * Update CSS to behave similar to MeasurementMain * Move search and download logic into samplemain and remove unnecessary logic and components * Remove unused logger * Make columns resizable, change sample label to name in spreadsheet and freeze action and id column * Reset search field value if page is refreshed due to any reason (project switch etc.) --------- Co-authored-by: Steffengreiner Co-authored-by: Steffengreiner --- .../sample/SamplePreviewJpaRepository.java | 8 +- .../themes/datamanager/components/main.css | 44 ++- .../datamanager/components/page-area.css | 24 +- .../samples/BatchDetailsComponent.java | 90 ++---- .../samples/SampleDetailsComponent.java | 283 +++++------------- .../samples/SampleInformationMain.java | 271 ++++++++++++----- .../SampleBatchInformationSpreadsheet.java | 2 +- 7 files changed, 339 insertions(+), 383 deletions(-) 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 8e0b4b3674..c68766542d 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/user-interface/frontend/themes/datamanager/components/main.css b/user-interface/frontend/themes/datamanager/components/main.css index b6daa17e3f..6d1ff8a95d 100644 --- a/user-interface/frontend/themes/datamanager/components/main.css +++ b/user-interface/frontend/themes/datamanager/components/main.css @@ -364,8 +364,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 +377,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 +477,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 9e22c9b2e8..a6c794db9a 100644 --- a/user-interface/frontend/themes/datamanager/components/page-area.css +++ b/user-interface/frontend/themes/datamanager/components/page-area.css @@ -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/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 3e55aed4a9..4de432b0a7 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 16dc2d8e9e..2229c0c385 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 18ffa85da6..d7676d9685 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/registration/batch/SampleBatchInformationSpreadsheet.java b/user-interface/src/main/java/life/qbic/datamanager/views/projects/project/samples/registration/batch/SampleBatchInformationSpreadsheet.java index 41f10c8463..f1d221d5bf 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();