From 1ed01e76301c792e1770233eec2c168541b62990 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Tue, 21 May 2024 16:23:57 +0200
Subject: [PATCH] =?UTF-8?q?S=C3=A9parer=20LeftPanel=20de=20Layout.=20refs?=
 =?UTF-8?q?=20#90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/migration.sql                             |  13 +
 .../www/client/presenter/LayoutPresenter.java | 127 +------
 .../client/presenter/LeftPanelPresenter.java  | 182 ++++++++++
 .../www/client/ui/chart/DailyValuesChart.java |   1 -
 .../www/client/view/LayoutView.java           | 282 +--------------
 .../www/client/view/LeftPanelView.java        | 331 ++++++++++++++++++
 www-server/pom.xml                            |   2 +-
 .../fr/agrometinfo/www/shared/module.gwt.xml  |   3 +-
 8 files changed, 548 insertions(+), 393 deletions(-)
 create mode 100644 www-client/src/main/java/fr/agrometinfo/www/client/presenter/LeftPanelPresenter.java
 create mode 100644 www-client/src/main/java/fr/agrometinfo/www/client/view/LeftPanelView.java

diff --git a/sql/migration.sql b/sql/migration.sql
index e3e2b7c..cc5cb5d 100644
--- a/sql/migration.sql
+++ b/sql/migration.sql
@@ -226,6 +226,19 @@ END
 $BODY$
 language plpgsql;
 
+--
+-- #88
+--
+CREATE OR REPLACE FUNCTION upgrade20240513() RETURNS boolean AS $BODY$
+BEGIN
+    UPDATE dailyvalue AS d
+        FROM normalvalue AS n ON n.indicator=d.indicator AND n.cell=d.cell AND n.doy=EXTRACT(DOY FROM d.date)
+        SET d.comparedvalue=COALESCE(d.computedvalue - n."medianvalue", 0);
+    RETURN true;
+END;
+$BODY$
+language plpgsql;
+
 ---
 --
 -- Keep this call at the end to apply migration functions.
diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/presenter/LayoutPresenter.java b/www-client/src/main/java/fr/agrometinfo/www/client/presenter/LayoutPresenter.java
index 73a1d82..e0575f3 100644
--- a/www-client/src/main/java/fr/agrometinfo/www/client/presenter/LayoutPresenter.java
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/presenter/LayoutPresenter.java
@@ -1,26 +1,17 @@
 package fr.agrometinfo.www.client.presenter;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 
 import org.dominokit.rest.shared.request.FailedResponseBean;
 
 import com.google.gwt.core.client.GWT;
 
-import fr.agrometinfo.www.client.App;
-import fr.agrometinfo.www.client.event.LoadingEvent;
-import fr.agrometinfo.www.client.event.LoadingEvent.LoadingEventType;
-import fr.agrometinfo.www.client.i18n.AppConstants;
 import fr.agrometinfo.www.client.view.BaseView;
 import fr.agrometinfo.www.client.view.LayoutView;
 import fr.agrometinfo.www.client.view.MapView;
 import fr.agrometinfo.www.client.view.RightPanelView;
 import fr.agrometinfo.www.shared.dto.ChoiceDTO;
 import fr.agrometinfo.www.shared.dto.IndicatorDTO;
-import fr.agrometinfo.www.shared.dto.PeriodDTO;
-import fr.agrometinfo.www.shared.service.IndicatorServiceFactory;
 
 /**
  * Presenter for the general layout.
@@ -46,27 +37,8 @@ public final class LayoutPresenter implements Presenter {
          */
         void setDevMode(boolean isDevMode);
 
-        /**
-         * @param list indicator periods with indicators
-         */
-        void setPeriods(List<PeriodDTO> list);
-
-        /**
-         * @param list regions with indicators
-         */
-        void setRegions(Map<String, String> list);
-
-        /**
-         * @param list years of computed values
-         */
-        void setYears(List<Integer> list);
     }
 
-    /**
-     * I18N constants.
-     */
-    private static final AppConstants CSTS = GWT.create(AppConstants.class);
-
     /**
      * Presenter for {@link MapView}.
      */
@@ -77,26 +49,11 @@ public final class LayoutPresenter implements Presenter {
      */
     private final RightPanelPresenter rightPanelPresenter = new RightPanelPresenter();
 
-    /**
-     * Periods from server.
-     */
-    private List<PeriodDTO> periods;
-
     /**
      * Related view.
      */
     private final LayoutView view = new LayoutView();
 
-    /**
-     * Region id : region name.
-     */
-    private final Map<String, String> regions = new HashMap<>();
-
-    /**
-     * Year chosen by the user.
-     */
-    private Integer chosenYear;
-
     /**
      * @see https://github.com/gwtproject/gwt/issues/7631#issuecomment-110876116
      * @return if the application is running in (Super) DevMode.
@@ -106,77 +63,19 @@ public final class LayoutPresenter implements Presenter {
     }
 
     /**
-     * @param choice the user's choice
+     * @param choice     the user's choice
+     * @param indicator  related indicator for the choice
+     * @param periodName name of chosen period
+     * @param regionName name of chosen region
      */
-    public void onChoiceChange(final ChoiceDTO choice) {
+    public void onChoiceChange(final ChoiceDTO choice, final IndicatorDTO indicator, final String periodName,
+            final String regionName) {
         GWT.log("LayoutPresenter.onChoiceChange()");
-        IndicatorDTO chosenIndicator = null;
-        String periodName = "";
-        for (final PeriodDTO period: periods) {
-            if (period.getCode().equals(choice.getPeriod())) {
-                periodName = period.getDescription();
-                for (final IndicatorDTO indicator : period.getIndicators()) {
-                    if (indicator.getCode().equals(choice.getIndicator())) {
-                        chosenIndicator = indicator;
-                    }
-                }
-            }
-        }
-        if ("0".equals(choice.getFeatureId())) {
-            choice.setFeatureId(null);
-        }
-        chosenYear = choice.getYear();
-        final String regionName = regions.getOrDefault(choice.getFeatureId(), CSTS.metropolitanFrance());
-        GWT.log("LayoutPresenter.onChoiceChange() a");
-        mapPresenter.loadValues(choice, chosenIndicator, periodName, regionName);
-        GWT.log("LayoutPresenter.onChoiceChange() b");
+        mapPresenter.loadValues(choice, indicator, periodName, regionName);
         rightPanelPresenter.loadValues(choice, periodName);
         GWT.log("LayoutPresenter.onChoiceChange() done");
     }
 
-    /**
-     * Memorize periods.
-     *
-     * @param list periods from server.
-     */
-    private void setPeriods(final List<PeriodDTO> list) {
-        this.periods = list;
-        view.setPeriods(list);
-
-        App.getEventBus().fireEvent(LoadingEvent.of(LoadingEventType.START));
-        IndicatorServiceFactory.INSTANCE.getRegions(chosenYear) //
-        .onSuccess(this::setRegions) //
-        .onFailed(view::failureNotification) //
-        .onComplete(() -> App.getEventBus().fireEvent(LoadingEvent.of(LoadingEventType.END))) //
-        .send();
-    }
-
-    /**
-     * @param list regions with indicators
-     */
-    private void setRegions(final Map<String, String> list) {
-        regions.clear();
-        regions.putAll(list);
-        view.setRegions(list);
-    }
-
-    /**
-     * @param list years of computed values
-     */
-    private void setYears(final List<Integer> list) {
-        view.setYears(list);
-
-        if (!list.isEmpty()) {
-            chosenYear = list.get(list.size() - 1);
-            App.getEventBus().fireEvent(LoadingEvent.of(LoadingEventType.START));
-            IndicatorServiceFactory.INSTANCE.getPeriods(chosenYear) //
-            .onSuccess(this::setPeriods) //
-            .onFailed(view::failureNotification) //
-            .onComplete(() -> App.getEventBus().fireEvent(LoadingEvent.of(LoadingEventType.END))) //
-            .send();
-        }
-    }
-
     /**
      * Display contact form.
      */
@@ -195,16 +94,14 @@ public final class LayoutPresenter implements Presenter {
         view.init();
         view.setDevMode(isDevMode());
 
+        final LeftPanelPresenter leftPanelPresenter = new LeftPanelPresenter();
+        leftPanelPresenter.setLayoutView(view);
+        leftPanelPresenter.setLayoutPresenter(this);
+        leftPanelPresenter.start();
+
         rightPanelPresenter.setLayoutView(view);
         rightPanelPresenter.start();
 
         new WelcomePresenter().start();
-
-        App.getEventBus().fireEvent(LoadingEvent.of(LoadingEventType.START));
-        IndicatorServiceFactory.INSTANCE.getYears() //
-        .onSuccess(this::setYears) //
-        .onFailed(view::failureNotification) //
-        .onComplete(() -> App.getEventBus().fireEvent(LoadingEvent.of(LoadingEventType.END))) //
-        .send();
     }
 }
diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/presenter/LeftPanelPresenter.java b/www-client/src/main/java/fr/agrometinfo/www/client/presenter/LeftPanelPresenter.java
new file mode 100644
index 0000000..5f1207c
--- /dev/null
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/presenter/LeftPanelPresenter.java
@@ -0,0 +1,182 @@
+package fr.agrometinfo.www.client.presenter;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gwt.core.client.GWT;
+
+import fr.agrometinfo.www.client.App;
+import fr.agrometinfo.www.client.event.LoadingEvent;
+import fr.agrometinfo.www.client.event.LoadingEvent.LoadingEventType;
+import fr.agrometinfo.www.client.i18n.AppConstants;
+import fr.agrometinfo.www.client.view.BaseView;
+import fr.agrometinfo.www.client.view.LayoutView;
+import fr.agrometinfo.www.client.view.LeftPanelView;
+import fr.agrometinfo.www.shared.dto.ChoiceDTO;
+import fr.agrometinfo.www.shared.dto.IndicatorDTO;
+import fr.agrometinfo.www.shared.dto.PeriodDTO;
+import fr.agrometinfo.www.shared.service.IndicatorServiceFactory;
+
+/**
+ * Presenter for the left panel in the LayoutView to display selectors.
+ *
+ * @author Olivier Maury
+ */
+public final class LeftPanelPresenter implements Presenter {
+
+    /**
+     * Related view interface.
+     */
+    public interface View extends BaseView<LeftPanelPresenter> {
+        /**
+         * @param list indicator periods with indicators
+         */
+        void setPeriods(List<PeriodDTO> list);
+
+        /**
+         * @param list regions with indicators
+         */
+        void setRegions(Map<String, String> list);
+
+        /**
+         * @param list years of computed values
+         */
+        void setYears(List<Integer> list);
+    }
+
+    /**
+     * I18N constants.
+     */
+    private static final AppConstants CSTS = GWT.create(AppConstants.class);
+
+    /**
+     * Periods from server.
+     */
+    private List<PeriodDTO> periods;
+
+    /**
+     * Region id : region name.
+     */
+    private final Map<String, String> regions = new HashMap<>();
+
+    /**
+     * Related view.
+     */
+    private View view;
+
+    /**
+     * The layout handling the panel.
+     */
+    private LayoutView layoutView;
+
+    /**
+     * The presenter for the application layout.
+     */
+    private LayoutPresenter layoutPresenter;
+    /**
+     * Year chosen by the user.
+     */
+    private Integer chosenYear;
+
+    /**
+     * @param choice the user's choice
+     */
+    public void onChoiceChange(final ChoiceDTO choice) {
+        GWT.log("LeftPanelPresenter.onChoiceChange()");
+        IndicatorDTO chosenIndicator = null;
+        String periodName = "";
+        for (final PeriodDTO period : periods) {
+            if (period.getCode().equals(choice.getPeriod())) {
+                periodName = period.getDescription();
+                for (final IndicatorDTO indicator : period.getIndicators()) {
+                    if (indicator.getCode().equals(choice.getIndicator())) {
+                        chosenIndicator = indicator;
+                    }
+                }
+            }
+        }
+        if ("0".equals(choice.getFeatureId())) {
+            choice.setFeatureId(null);
+        }
+        chosenYear = choice.getYear();
+        final String regionName = regions.getOrDefault(choice.getFeatureId(), CSTS.metropolitanFrance());
+        layoutPresenter.onChoiceChange(choice, chosenIndicator, periodName, regionName);
+    }
+
+    /**
+     * @param lPresenter presenter of LayoutView
+     */
+    public void setLayoutPresenter(final LayoutPresenter lPresenter) {
+        layoutPresenter = lPresenter;
+    }
+
+    /**
+     * @param lView the layout handling the panel.
+     */
+    public void setLayoutView(final LayoutView lView) {
+        layoutView = lView;
+    }
+
+    /**
+     * Memorize periods.
+     *
+     * @param list periods from server.
+     */
+    private void setPeriods(final List<PeriodDTO> list) {
+        GWT.log("RightPanelPresenter.setPeriods()");
+        this.periods = list;
+        view.setPeriods(list);
+
+        App.getEventBus().fireEvent(LoadingEvent.of(LoadingEventType.START));
+        IndicatorServiceFactory.INSTANCE.getRegions(chosenYear) //
+        .onSuccess(this::setRegions) //
+        .onFailed(layoutView::failureNotification) //
+        .onComplete(() -> App.getEventBus().fireEvent(LoadingEvent.of(LoadingEventType.END))) //
+        .send();
+    }
+
+    /**
+     * @param list regions with indicators
+     */
+    private void setRegions(final Map<String, String> list) {
+        GWT.log("RightPanelPresenter.setRegions()");
+        regions.clear();
+        regions.putAll(list);
+        view.setRegions(list);
+    }
+
+    /**
+     * @param list years of computed values
+     */
+    private void setYears(final List<Integer> list) {
+        GWT.log("RightPanelPresenter.setYears()");
+        view.setYears(list);
+
+        if (!list.isEmpty()) {
+            chosenYear = list.get(list.size() - 1);
+            App.getEventBus().fireEvent(LoadingEvent.of(LoadingEventType.START));
+            IndicatorServiceFactory.INSTANCE.getPeriods(chosenYear) //
+            .onSuccess(this::setPeriods) //
+            .onFailed(layoutView::failureNotification) //
+            .onComplete(() -> App.getEventBus().fireEvent(LoadingEvent.of(LoadingEventType.END))) //
+            .send();
+        }
+    }
+
+    @Override
+    public void start() {
+        GWT.log("RightPanelPresenter.start()");
+        view = new LeftPanelView(layoutView.getLeftPanel());
+        view.setPresenter(this);
+        view.init();
+
+        App.getEventBus().fireEvent(LoadingEvent.of(LoadingEventType.START));
+        IndicatorServiceFactory.INSTANCE.getYears() //
+        .onSuccess(this::setYears) //
+        .onFailed(layoutView::failureNotification) //
+        .onComplete(() -> App.getEventBus().fireEvent(LoadingEvent.of(LoadingEventType.END))) //
+        .send();
+
+    }
+}
diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/ui/chart/DailyValuesChart.java b/www-client/src/main/java/fr/agrometinfo/www/client/ui/chart/DailyValuesChart.java
index 565e2fe..64b76c7 100644
--- a/www-client/src/main/java/fr/agrometinfo/www/client/ui/chart/DailyValuesChart.java
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/ui/chart/DailyValuesChart.java
@@ -95,7 +95,6 @@ public final class DailyValuesChart extends TimeSeriesLineChart {
         final LinkedList<TimeSeriesItem> q95Data = new LinkedList<>();
 
         values.forEach((date, value) -> {
-            GWT.log("DailyValuesChart() " + date + "->" + value[SummaryDTO.COMPUTED_INDEX]);
             data.add(new TimeSeriesItem(date, value[SummaryDTO.COMPUTED_INDEX]));
             if (value[SummaryDTO.Q5_INDEX] != null) {
                 q5Data.add(new TimeSeriesItem(date, value[SummaryDTO.Q5_INDEX]));
diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java b/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java
index 9708094..5f7e8ba 100644
--- a/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java
@@ -5,20 +5,13 @@ import static org.jboss.elemento.Elements.div;
 import static org.jboss.elemento.Elements.img;
 import static org.jboss.elemento.Elements.li;
 
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Optional;
 import java.util.StringJoiner;
 
-import org.dominokit.domino.ui.forms.Select;
-import org.dominokit.domino.ui.forms.SwitchButton;
 import org.dominokit.domino.ui.grid.flex.FlexItem;
 import org.dominokit.domino.ui.icons.Icons;
 import org.dominokit.domino.ui.icons.MdiIcon;
 import org.dominokit.domino.ui.layout.Layout;
-import org.dominokit.domino.ui.lists.ListGroup;
 import org.dominokit.domino.ui.loaders.Loader;
 import org.dominokit.domino.ui.loaders.LoaderEffect;
 import org.dominokit.domino.ui.menu.Menu;
@@ -41,7 +34,6 @@ import elemental2.dom.DomGlobal;
 import elemental2.dom.HTMLAnchorElement;
 import elemental2.dom.HTMLDivElement;
 import elemental2.dom.HTMLElement;
-import elemental2.dom.HTMLLabelElement;
 import elemental2.dom.MouseEvent;
 import fr.agrometinfo.www.client.App;
 import fr.agrometinfo.www.client.event.LoadingEvent;
@@ -52,14 +44,8 @@ import fr.agrometinfo.www.client.presenter.LayoutPresenter;
 import fr.agrometinfo.www.client.presenter.MapPresenter;
 import fr.agrometinfo.www.client.presenter.RightPanelPresenter;
 import fr.agrometinfo.www.client.ui.AgroclimAppsMenu;
-import fr.agrometinfo.www.client.ui.CollapsedSelect;
-import fr.agrometinfo.www.client.ui.DominoListBuilder;
-import fr.agrometinfo.www.client.ui.DominoSelectBuilder;
 import fr.agrometinfo.www.client.util.ApplicationUtils;
-import fr.agrometinfo.www.shared.dto.ChoiceDTO;
-import fr.agrometinfo.www.shared.dto.FeatureLevel;
-import fr.agrometinfo.www.shared.dto.IndicatorDTO;
-import fr.agrometinfo.www.shared.dto.PeriodDTO;
+
 
 /**
  * View for the general layout.
@@ -73,15 +59,6 @@ implements LayoutPresenter.View, LoadingHandler {
      */
     private static final AppConstants CSTS = GWT.create(AppConstants.class);
 
-    /**
-     * Code of indicator to show by default.
-     */
-    private static final String DEFAULT_INDICATOR = "meant";
-
-    /**
-     * Code of period to show by default.
-     */
-    private static final String DEFAULT_PERIOD = "year";
 
     /**
      * The minimum screen width to display panels on startup.
@@ -136,11 +113,6 @@ implements LayoutPresenter.View, LoadingHandler {
         }
     }
 
-    /**
-     * Choice from the user.
-     */
-    private final ChoiceDTO choice = new ChoiceDTO();
-
     /**
      * Drop down menu for user actions.
      */
@@ -151,11 +123,6 @@ implements LayoutPresenter.View, LoadingHandler {
      */
     private final Menu<String> dotDropMenu = Menu.<String>create().setUseSmallScreensDirection(false);
 
-    /**
-     * Selector for indicators.
-     */
-    private final ListGroup<IndicatorDTO> indicatorSelect = ListGroup.create();
-
     /**
      * Application layout.
      */
@@ -177,36 +144,11 @@ implements LayoutPresenter.View, LoadingHandler {
      */
     private MapPresenter mapPresenter;
 
-    /**
-     * Available indicator periods with indicators.
-     */
-    private List<PeriodDTO> periods;
-
-    /**
-     * Selector for indicator periods.
-     */
-    private final Select<PeriodDTO> periodSelect = Select.create(CSTS.choosePeriod());
-
-    /**
-     * Selector for regions.
-     */
-    private final Select<Entry<String, String>> regionSelect = Select.create(CSTS.chooseRegion());
-
     /**
      * Presenter for {@link RightPanelView}.
      */
     private RightPanelPresenter rightPanelPresenter;
 
-    /**
-     * Summary of available indicators.
-     */
-    private final HtmlContentBuilder<HTMLDivElement> summary = div().css("p-b-15");
-
-    /**
-     * Selector for year.
-     */
-    private final Select<Entry<String, String>> yearSelect = Select.create(CSTS.chooseYear());
-
     /**
      * Counter for loading events.
      */
@@ -222,11 +164,6 @@ implements LayoutPresenter.View, LoadingHandler {
      */
     private final HtmlContentBuilder<HTMLDivElement> links = div().css("links");
 
-    /**
-     * The replacement of indicatorSelect when it is hidden.
-     */
-    private final CollapsedSelect collapsedIndicatorSelect = new CollapsedSelect(CSTS.chooseIndicator(), "");
-
     /**
      * @param text     link text
      * @param callback {@link EventCallbackFn<MouseEvent>} to be added to the click
@@ -259,6 +196,13 @@ implements LayoutPresenter.View, LoadingHandler {
         this.notification(MSGS.failureResponse(getDetails(failedResponse)));
     }
 
+    /**
+     * @return the left panel {@link HTMLElement} wrapped as a {@link DominoElement}
+     */
+    public DominoElement<HTMLElement> getLeftPanel() {
+        return layout.getLeftPanel();
+    }
+
     /**
      * Close the left panel if it is open.
      */
@@ -272,7 +216,6 @@ implements LayoutPresenter.View, LoadingHandler {
     @Override
     public void init() {
         GWT.log("LayoutView.initLayout()");
-        choice.setLevel(FeatureLevel.REGION);
         layout = Layout.create("AgroMetInfo").show();
         layout.css("app-layout");
         final HTMLDivElement contentElement = div().element();
@@ -339,77 +282,6 @@ implements LayoutPresenter.View, LoadingHandler {
                 }) //
                 .add(Icons.CONTENT_ICONS.clear().clickable()));
 
-        panel.appendChild(summary);
-
-        //
-        GWT.log("LayoutView.initLeftPanel() periods");
-        panel.appendChild(periodSelect);
-        new DominoSelectBuilder<PeriodDTO>() //
-        .setSelect(periodSelect) //
-        .addValueChangeHandler(this::onPeriodChange) //
-        .build();
-        periodSelect.setSearchable(false);
-
-        //
-        GWT.log("LayoutView.initLeftPanel() indicators");
-        collapsedIndicatorSelect.hide();
-        panel.appendChild(collapsedIndicatorSelect.getRoot());
-        final HtmlContentBuilder<HTMLDivElement> indicatorPanel = div().css("field-group lined list-group");
-        final HtmlContentBuilder<HTMLLabelElement> indicatorLabel = Elements.label() //
-                .css("field-label") //
-                .add(Elements.span().textContent(CSTS.chooseIndicator()));
-        panel.appendChild(indicatorPanel//
-                .add(indicatorLabel) //
-                .add(indicatorSelect) //
-                );
-        new DominoListBuilder<IndicatorDTO>() //
-        .setSelect(indicatorSelect) //
-        .addTextChangeHandler(collapsedIndicatorSelect::setSelectionText) //
-        .setTextFunction(IndicatorDTO::getDescription) //
-        .setValueFunction(IndicatorDTO::getCode) //
-        .addValueChangeHandler(this::onIndicatorChange) //
-        .build();
-
-        indicatorLabel.on(EventType.click, e -> {
-            indicatorPanel.hidden(true);
-            collapsedIndicatorSelect.show();
-        });
-        collapsedIndicatorSelect.onClick(e -> {
-            indicatorPanel.hidden(false);
-            collapsedIndicatorSelect.hide();
-        });
-
-        //
-        GWT.log("LayoutView.initLeftPanel() regions");
-        panel.appendChild(regionSelect);
-        new DominoSelectBuilder<Entry<String, String>>() //
-        .setSelect(regionSelect) //
-        .addValueChangeHandler(this::onRegionChange) //
-        .build();
-        regionSelect.setSearchable(false);
-
-        //
-        GWT.log("LayoutView.initLeftPanel() year");
-        panel.appendChild(yearSelect);
-        new DominoSelectBuilder<Entry<String, String>>() //
-        .setSelect(yearSelect) //
-        .addValueChangeHandler(this::onYearChange);
-        yearSelect.setSearchable(false);
-
-        //
-        GWT.log("LayoutView.initLeftPanel() comparison");
-        final SwitchButton comparisonBtn = SwitchButton.create(CSTS.normalComparison(), CSTS.no(), CSTS.yes()) //
-                .value(false);
-        comparisonBtn.addChangeHandler(this::onComparisonChange);
-        comparisonBtn.addCss("comparison-btn");
-        final HTMLElement info = Elements.createElement("details", HTMLElement.class);
-        final HTMLElement sum = Elements.createElement("summary", HTMLElement.class);
-        sum.innerHTML = "<i class='material-icons'>info_outline</i>";
-        info.innerHTML = CSTS.normalComparisonTooltip();
-        info.append(sum);
-        comparisonBtn.getFieldContainer().getFirstChild().appendChild(info);
-        panel.appendChild(comparisonBtn);
-
         // finally
         layout.showLeftPanel();
     }
@@ -522,25 +394,6 @@ implements LayoutPresenter.View, LoadingHandler {
         return Notification.create(msg).setPosition(Notification.TOP_LEFT).show();
     }
 
-    private void onChoiceChange() {
-        GWT.log("LayoutView.onChoiceChange()");
-        if (choice.isValid()) {
-            getPresenter().onChoiceChange(choice);
-        }
-    }
-
-    private void onComparisonChange(final boolean newValue) {
-        GWT.log("LayoutView.onComparisonChange()");
-        choice.setComparison(newValue);
-        onChoiceChange();
-    }
-
-    private void onIndicatorChange(final String newValue) {
-        GWT.log("LayoutView.onIndicatorChange() " + newValue);
-        choice.setIndicator(newValue);
-        onChoiceChange();
-    }
-
     @Override
     public void onLoading(final LoadingEvent event) {
         GWT.log("LayoutView.onLoading() " + event);
@@ -565,64 +418,6 @@ implements LayoutPresenter.View, LoadingHandler {
         GWT.log("LayoutView.onLoading() done");
     }
 
-    private void onPeriodChange(final String newValue) {
-        GWT.log("LayoutView.onPeriodChange() " + newValue);
-        choice.setPeriod(newValue);
-        choice.setIndicator(null);
-
-        // fill indicators
-        List<IndicatorDTO> list = null;
-        for (final PeriodDTO p : this.periods) {
-            if (p.getCode().equals(newValue)) {
-                list = p.getIndicators();
-                break;
-            }
-        }
-        if (list == null || list.isEmpty()) {
-            indicatorSelect.clearElement();
-            return;
-        }
-        GWT.log("LayoutView.onPeriodChange() Indicators : " + list);
-        final IndicatorDTO firstIndicator = list.get(0);
-        final IndicatorDTO defaultIndicator = list.stream() //
-                .filter(i -> DEFAULT_INDICATOR.equals(i.getCode())) //
-                .findFirst().orElse(firstIndicator);
-        new DominoListBuilder<IndicatorDTO>() //
-        .setSelect(indicatorSelect) //
-        .setTextFunction(IndicatorDTO::getDescription) //
-        .setValueFunction(IndicatorDTO::getCode) //
-        .removeOptions() //
-        .addOptions(list) //
-        .select(defaultIndicator) //
-        .build();
-        // select "meant"
-        if (defaultIndicator != null) {
-            onIndicatorChange(defaultIndicator.getCode());
-        } else {
-            onChoiceChange();
-        }
-    }
-
-    private void onRegionChange(final String newValue) {
-        GWT.log("LayoutView.onRegionChange()");
-        if (newValue.length() > 2) {
-            choice.setFeatureId(null);
-        } else {
-            choice.setFeatureId(newValue);
-        }
-        onChoiceChange();
-    }
-
-    private void onYearChange(final String newValue) {
-        GWT.log("LayoutView.onYearChange()");
-        try {
-            choice.setYear(Integer.valueOf(newValue));
-        } catch (final NumberFormatException e) {
-            choice.setYear(null);
-        }
-        onChoiceChange();
-    }
-
     /**
      * To display an OpenLayers map, a container with a fixed height CSS property is
      * needed.
@@ -670,50 +465,6 @@ implements LayoutPresenter.View, LoadingHandler {
         this.mapPresenter = presenter;
     }
 
-    @Override
-    public void setPeriods(final List<PeriodDTO> list) {
-        this.periods = list;
-        // summary
-        final StringJoiner sj = new StringJoiner(" ");
-        sj.add(MSGS.nbOfIndicatorPeriods(list.size()));
-        sj.add(MSGS.nbOfIndicators(list.stream().mapToInt(c -> c.getIndicators().size()).sum()));
-        summary.textContent(sj.toString());
-
-        if (this.periods.isEmpty()) {
-            return;
-        }
-        // display periods
-        GWT.log("LayoutView.setPeriods() : " + list);
-        final PeriodDTO defaultPeriod = list.stream() //
-                .filter(i -> DEFAULT_PERIOD.equals(i.getCode())) //
-                .findFirst().orElse(null);
-
-        new DominoSelectBuilder<PeriodDTO>() //
-        .setSelect(periodSelect) //
-        .setTextFunction(PeriodDTO::getDescription) //
-        .setValueFunction(PeriodDTO::getCode) //
-        .addOptions(list) //
-        .select(defaultPeriod) //
-        .build();
-    }
-
-    @Override
-    public void setRegions(final Map<String, String> list) {
-        GWT.log("LayoutView.setRegions()");
-        final String metropolitanFranceCode = "0";
-        list.put(metropolitanFranceCode, CSTS.metropolitanFrance());
-        final Entry<String, String> defaultRegion = list.entrySet().stream() //
-                .filter(e -> metropolitanFranceCode.equals(e.getKey())) //
-                .findFirst().orElse(null);
-        new DominoSelectBuilder<Entry<String, String>>() //
-        .setSelect(regionSelect) //
-        .setTextFunction(Entry<String, String>::getValue) //
-        .setValueFunction(Entry<String, String>::getKey) //
-        .addOptions(list.entrySet()) //
-        .select(defaultRegion) //
-        .build();
-    }
-
     /**
      * @param presenter presenter for {@link RightPanelView}.
      */
@@ -721,23 +472,6 @@ implements LayoutPresenter.View, LoadingHandler {
         this.rightPanelPresenter = presenter;
     }
 
-    @Override
-    public void setYears(final List<Integer> list) {
-        GWT.log("LayoutView.setYears()");
-        final Map<String, String> yearOptions = new HashMap<>();
-        list.forEach(y -> yearOptions.put(String.valueOf(y), String.valueOf(y)));
-        final Optional<Integer> defaultYear = list.stream().max(Integer::compare);
-        final Entry<String, String> yearEntry = yearOptions.entrySet().stream() //
-                .filter(e -> e.getKey().equals(String.valueOf(defaultYear.get()))) //
-                .findFirst().orElse(null);
-        new DominoSelectBuilder<Entry<String, String>>() //
-        .setSelect(yearSelect) //
-        .setTextFunction(Entry<String, String>::getValue) //
-        .setValueFunction(Entry<String, String>::getKey) //
-        .select(yearEntry) //
-        .addOptions(yearOptions.entrySet());
-    }
-
     /**
      * Open the right panel.
      */
diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/view/LeftPanelView.java b/www-client/src/main/java/fr/agrometinfo/www/client/view/LeftPanelView.java
new file mode 100644
index 0000000..7918406
--- /dev/null
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/view/LeftPanelView.java
@@ -0,0 +1,331 @@
+package fr.agrometinfo.www.client.view;
+
+import static org.jboss.elemento.Elements.div;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.StringJoiner;
+
+import org.dominokit.domino.ui.forms.Select;
+import org.dominokit.domino.ui.forms.SwitchButton;
+import org.dominokit.domino.ui.lists.ListGroup;
+import org.dominokit.domino.ui.utils.DominoElement;
+import org.jboss.elemento.Elements;
+import org.jboss.elemento.EventType;
+import org.jboss.elemento.HtmlContentBuilder;
+
+import com.google.gwt.core.client.GWT;
+
+import elemental2.dom.HTMLDivElement;
+import elemental2.dom.HTMLElement;
+import elemental2.dom.HTMLLabelElement;
+import fr.agrometinfo.www.client.i18n.AppConstants;
+import fr.agrometinfo.www.client.i18n.AppMessages;
+import fr.agrometinfo.www.client.presenter.LeftPanelPresenter;
+import fr.agrometinfo.www.client.ui.CollapsedSelect;
+import fr.agrometinfo.www.client.ui.DominoListBuilder;
+import fr.agrometinfo.www.client.ui.DominoSelectBuilder;
+import fr.agrometinfo.www.shared.dto.ChoiceDTO;
+import fr.agrometinfo.www.shared.dto.FeatureLevel;
+import fr.agrometinfo.www.shared.dto.IndicatorDTO;
+import fr.agrometinfo.www.shared.dto.PeriodDTO;
+
+/**
+ * Right panel in the LayoutView to display data related to user choice.
+ *
+ * @author Olivier Maury
+ */
+public final class LeftPanelView extends AbstractBaseView<LeftPanelPresenter> implements LeftPanelPresenter.View {
+    /**
+     * I18N constants.
+     */
+    private static final AppConstants CSTS = GWT.create(AppConstants.class);
+
+    /**
+     * Code of indicator to show by default.
+     */
+    private static final String DEFAULT_INDICATOR = "meant";
+
+    /**
+     * Code of period to show by default.
+     */
+    private static final String DEFAULT_PERIOD = "year";
+
+    /**
+     * I18N messages.
+     */
+    private static final AppMessages MSGS = GWT.create(AppMessages.class);
+
+    /**
+     * Layout handling the panel.
+     */
+    private final DominoElement<HTMLElement> panel;
+
+    /**
+     * Summary of available indicators.
+     */
+    private final HtmlContentBuilder<HTMLDivElement> summary = div().css("p-b-15");
+
+    /**
+     * Selector for indicator periods.
+     */
+    private final Select<PeriodDTO> periodSelect = Select.create(CSTS.choosePeriod());
+
+    /**
+     * Choice from the user.
+     */
+    private final ChoiceDTO choice = new ChoiceDTO();
+
+    /**
+     * Available indicator periods with indicators.
+     */
+    private List<PeriodDTO> periods;
+
+    /**
+     * Selector for indicators.
+     */
+    private final ListGroup<IndicatorDTO> indicatorSelect = ListGroup.create();
+
+    /**
+     * Selector for regions.
+     */
+    private final Select<Entry<String, String>> regionSelect = Select.create(CSTS.chooseRegion());
+
+    /**
+     * The replacement of indicatorSelect when it is hidden.
+     */
+    private final CollapsedSelect collapsedIndicatorSelect = new CollapsedSelect(CSTS.chooseIndicator(), "");
+
+    /**
+     * Selector for year.
+     */
+    private final Select<Entry<String, String>> yearSelect = Select.create(CSTS.chooseYear());
+
+    /**
+     * Constructor.
+     *
+     * @param leftPanel container for the panel.
+     */
+    public LeftPanelView(final DominoElement<HTMLElement> leftPanel) {
+        GWT.log("LeftPanelView()");
+        panel = leftPanel;
+    }
+
+    @Override
+    public void init() {
+
+        choice.setLevel(FeatureLevel.REGION);
+
+        panel.appendChild(summary);
+
+        //
+        GWT.log("LeftPanelView.initLeftPanel() periods");
+        panel.appendChild(periodSelect);
+        new DominoSelectBuilder<PeriodDTO>() //
+        .setSelect(periodSelect) //
+        .addValueChangeHandler(this::onPeriodChange) //
+        .build();
+        periodSelect.setSearchable(false);
+
+        //
+        GWT.log("LeftPanelView.initLeftPanel() indicators");
+        collapsedIndicatorSelect.hide();
+        panel.appendChild(collapsedIndicatorSelect.getRoot());
+        final HtmlContentBuilder<HTMLDivElement> indicatorPanel = div().css("field-group lined list-group");
+        final HtmlContentBuilder<HTMLLabelElement> indicatorLabel = Elements.label() //
+                .css("field-label") //
+                .add(Elements.span().textContent(CSTS.chooseIndicator()));
+        panel.appendChild(indicatorPanel//
+                .add(indicatorLabel) //
+                .add(indicatorSelect) //
+                );
+        new DominoListBuilder<IndicatorDTO>() //
+        .setSelect(indicatorSelect) //
+        .addTextChangeHandler(collapsedIndicatorSelect::setSelectionText) //
+        .setTextFunction(IndicatorDTO::getDescription) //
+        .setValueFunction(IndicatorDTO::getCode) //
+        .addValueChangeHandler(this::onIndicatorChange) //
+        .build();
+
+        indicatorLabel.on(EventType.click, e -> {
+            indicatorPanel.hidden(true);
+            collapsedIndicatorSelect.show();
+        });
+        collapsedIndicatorSelect.onClick(e -> {
+            indicatorPanel.hidden(false);
+            collapsedIndicatorSelect.hide();
+        });
+
+        //
+        GWT.log("LeftPanelView.initLeftPanel() regions");
+        panel.appendChild(regionSelect);
+        new DominoSelectBuilder<Entry<String, String>>() //
+        .setSelect(regionSelect) //
+        .addValueChangeHandler(this::onRegionChange) //
+        .build();
+        regionSelect.setSearchable(false);
+
+        //
+        GWT.log("LeftPanelView.initLeftPanel() year");
+        panel.appendChild(yearSelect);
+        new DominoSelectBuilder<Entry<String, String>>() //
+        .setSelect(yearSelect) //
+        .addValueChangeHandler(this::onYearChange);
+        yearSelect.setSearchable(false);
+
+        //
+        GWT.log("LeftPanelView.initLeftPanel() comparison");
+        final SwitchButton comparisonBtn = SwitchButton.create(CSTS.normalComparison(), CSTS.no(), CSTS.yes()) //
+                .value(false);
+        comparisonBtn.addChangeHandler(this::onComparisonChange);
+        comparisonBtn.addCss("comparison-btn");
+        final HTMLElement info = Elements.createElement("details", HTMLElement.class);
+        final HTMLElement sum = Elements.createElement("summary", HTMLElement.class);
+        sum.innerHTML = "<i class='material-icons'>info_outline</i>";
+        info.innerHTML = CSTS.normalComparisonTooltip();
+        info.append(sum);
+        comparisonBtn.getFieldContainer().getFirstChild().appendChild(info);
+        panel.appendChild(comparisonBtn);
+    }
+
+    private void onChoiceChange() {
+        GWT.log("LeftPanelView.onChoiceChange()");
+        if (choice.isValid()) {
+            getPresenter().onChoiceChange(choice);
+        }
+    }
+
+    private void onComparisonChange(final boolean newValue) {
+        GWT.log("LeftPanelView.onComparisonChange()");
+        choice.setComparison(newValue);
+        onChoiceChange();
+    }
+
+    private void onIndicatorChange(final String newValue) {
+        GWT.log("LeftPanelView.onIndicatorChange() " + newValue);
+        choice.setIndicator(newValue);
+        onChoiceChange();
+    }
+
+    private void onPeriodChange(final String newValue) {
+        GWT.log("LeftPanelView.onPeriodChange() " + newValue);
+        choice.setPeriod(newValue);
+        choice.setIndicator(null);
+
+        // fill indicators
+        List<IndicatorDTO> list = null;
+        for (final PeriodDTO p : this.periods) {
+            if (p.getCode().equals(newValue)) {
+                list = p.getIndicators();
+                break;
+            }
+        }
+        if (list == null || list.isEmpty()) {
+            indicatorSelect.clearElement();
+            return;
+        }
+        GWT.log("LeftPanelView.onPeriodChange() Indicators : " + list);
+        final IndicatorDTO firstIndicator = list.get(0);
+        final IndicatorDTO defaultIndicator = list.stream() //
+                .filter(i -> DEFAULT_INDICATOR.equals(i.getCode())) //
+                .findFirst().orElse(firstIndicator);
+        new DominoListBuilder<IndicatorDTO>() //
+        .setSelect(indicatorSelect) //
+        .setTextFunction(IndicatorDTO::getDescription) //
+        .setValueFunction(IndicatorDTO::getCode) //
+        .removeOptions() //
+        .addOptions(list) //
+        .select(defaultIndicator);
+        GWT.log("LeftPanelView.onPeriodChange() defaultIndicator : " + defaultIndicator);
+        // select "meant"
+        if (defaultIndicator != null) {
+            onIndicatorChange(defaultIndicator.getCode());
+        } else {
+            onChoiceChange();
+        }
+    }
+
+    private void onRegionChange(final String newValue) {
+        GWT.log("LeftPanelView.onRegionChange()");
+        if (newValue.length() > 2) {
+            choice.setFeatureId(null);
+        } else {
+            choice.setFeatureId(newValue);
+        }
+        onChoiceChange();
+    }
+
+    private void onYearChange(final String newValue) {
+        GWT.log("LeftPanelView.onYearChange()");
+        try {
+            choice.setYear(Integer.valueOf(newValue));
+        } catch (final NumberFormatException e) {
+            choice.setYear(null);
+        }
+        onChoiceChange();
+    }
+
+    @Override
+    public void setPeriods(final List<PeriodDTO> list) {
+        GWT.log("LeftPanelView.setPeriods() : " + list);
+        this.periods = list;
+        // summary
+        final StringJoiner sj = new StringJoiner(" ");
+        sj.add(MSGS.nbOfIndicatorPeriods(list.size()));
+        sj.add(MSGS.nbOfIndicators(list.stream().mapToInt(c -> c.getIndicators().size()).sum()));
+        summary.textContent(sj.toString());
+
+        if (this.periods.isEmpty()) {
+            return;
+        }
+        // display periods
+        final PeriodDTO defaultPeriod = list.stream() //
+                .filter(i -> DEFAULT_PERIOD.equals(i.getCode())) //
+                .findFirst().orElse(null);
+
+        new DominoSelectBuilder<PeriodDTO>() //
+        .setSelect(periodSelect) //
+        .setTextFunction(PeriodDTO::getDescription) //
+        .setValueFunction(PeriodDTO::getCode) //
+        .addOptions(list) //
+        .select(defaultPeriod) //
+        .build();
+    }
+
+    @Override
+    public void setRegions(final Map<String, String> list) {
+        GWT.log("LeftPanelView.setRegions()");
+        final String metropolitanFranceCode = "0";
+        list.put(metropolitanFranceCode, CSTS.metropolitanFrance());
+        final Entry<String, String> defaultRegion = list.entrySet().stream() //
+                .filter(e -> metropolitanFranceCode.equals(e.getKey())) //
+                .findFirst().orElse(null);
+        new DominoSelectBuilder<Entry<String, String>>() //
+        .setSelect(regionSelect) //
+        .setTextFunction(Entry<String, String>::getValue) //
+        .setValueFunction(Entry<String, String>::getKey) //
+        .addOptions(list.entrySet()) //
+        .select(defaultRegion) //
+        .build();
+    }
+
+    @Override
+    public void setYears(final List<Integer> list) {
+        GWT.log("LeftPanelView.setYears()");
+        final Map<String, String> yearOptions = new HashMap<>();
+        list.forEach(y -> yearOptions.put(String.valueOf(y), String.valueOf(y)));
+        final Optional<Integer> defaultYear = list.stream().max(Integer::compare);
+        final Entry<String, String> yearEntry = yearOptions.entrySet().stream() //
+                .filter(e -> e.getKey().equals(String.valueOf(defaultYear.get()))) //
+                .findFirst().orElse(null);
+        new DominoSelectBuilder<Entry<String, String>>() //
+        .setSelect(yearSelect) //
+        .setTextFunction(Entry<String, String>::getValue) //
+        .setValueFunction(Entry<String, String>::getKey) //
+        .select(yearEntry) //
+        .addOptions(yearOptions.entrySet());
+    }
+}
diff --git a/www-server/pom.xml b/www-server/pom.xml
index 89c7e39..783b50e 100644
--- a/www-server/pom.xml
+++ b/www-server/pom.xml
@@ -164,7 +164,7 @@
     <dependency>
       <groupId>org.glassfish.web</groupId>
       <artifactId>jakarta.servlet.jsp.jstl</artifactId>
-      <version>3.0.0</version>
+      <version>3.0.1</version>
     </dependency>
     <!-- fast-serialization -->
     <!-- https://mvnrepository.com/artifact/de.ruedigermoeller/fst -->
diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/module.gwt.xml b/www-shared/src/main/java/fr/agrometinfo/www/shared/module.gwt.xml
index 3d37b27..164dd77 100644
--- a/www-shared/src/main/java/fr/agrometinfo/www/shared/module.gwt.xml
+++ b/www-shared/src/main/java/fr/agrometinfo/www/shared/module.gwt.xml
@@ -1,6 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.9.0//EN"
-    "https://www.gwtproject.org/doctype/2.9.0/gwt-module.dtd">
+<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.11.0//EN" "https://www.gwtproject.org/doctype/2.11.0/gwt-module.dtd">
 <module>
   <inherits name="org.dominokit.rest.Rest"/>
   <inherits name="org.dominokit.jackson.Jackson"/>
-- 
GitLab