fix: add a global dashboard filter bar (#24)
CI / build-and-test (pull_request) Successful in 2m46s
CI / build-and-test (pull_request) Successful in 2m46s
The reporting period was baked into the data service, so nothing could change what the dashboard shows without editing code. - DashboardFilter (period + optional region) parameterises every ChartDataService query; the no-arg overloads are the default filter. - DashboardContext is the bus between the new DashboardFilterBar and the widgets: charts subscribe in WidgetRegistry, KPI tiles in DashboardView. - Updates go through updateData, so widgets patch in place instead of being rebuilt; SparklineChart gained the same path. - KpiData carries the number, the bundle the unit and number pattern, so a KPI value can follow the filter and stay locale-formatted. - Filter state is deliberately not persisted, unlike the grid layout. Closes #24 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69
This commit is contained in:
@@ -6,7 +6,9 @@ import com.example.components.GridStackItem;
|
||||
import com.example.components.GridStackLayout;
|
||||
import com.example.components.KpiTile;
|
||||
import com.example.data.ChartDataService;
|
||||
import com.example.data.DashboardFilter;
|
||||
import com.example.data.KpiData;
|
||||
import com.example.widgets.DashboardContext;
|
||||
import com.example.widgets.WidgetDefinition;
|
||||
import com.example.widgets.WidgetRegistry;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
@@ -19,7 +21,9 @@ import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.HasDynamicTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The dashboard: a {@link GridStackLayout} of draggable/resizable cards whose
|
||||
@@ -31,6 +35,11 @@ import java.util.List;
|
||||
* All widget numbers come from {@link ChartDataService} and every chart widget
|
||||
* is built by the registry; this view only decides where a widget sits and
|
||||
* resolves the translation keys it is handed against the bundle.
|
||||
* <p>
|
||||
* A {@link DashboardFilterBar} above the grid drives every widget through one
|
||||
* {@link DashboardContext}: the charts subscribe to it themselves (in the
|
||||
* registry), the KPI tiles are re-fed here, since this view is what built them.
|
||||
* The filter is view state and is not persisted — see {@link DashboardContext}.
|
||||
*/
|
||||
@Route("")
|
||||
public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
||||
@@ -40,12 +49,18 @@ public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
||||
/** A KPI tile is a quarter row wide, so the n-th one starts at 3n. */
|
||||
private static final int KPI_WIDTH = 3;
|
||||
|
||||
private final ChartDataService dataService;
|
||||
private final WidgetRegistry widgets;
|
||||
private final DashboardContext context = new DashboardContext();
|
||||
private final GridStackLayout grid = new GridStackLayout();
|
||||
private final Span status = new Span();
|
||||
/** The KPI tiles by KPI id, so a filter change re-feeds each tile with the
|
||||
* data of the same KPI rather than by position. */
|
||||
private final Map<String, KpiTile> kpiTiles = new LinkedHashMap<>();
|
||||
private int extraWidgetCount;
|
||||
|
||||
public DashboardView(ChartDataService dataService, WidgetRegistry widgets) {
|
||||
this.dataService = dataService;
|
||||
this.widgets = widgets;
|
||||
addClassName("dialect-content");
|
||||
|
||||
@@ -58,12 +73,14 @@ public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
||||
// that explain them. They are 3x1 — a quarter row each, one cell high,
|
||||
// laid out left to right in the order the service returns them. The
|
||||
// grid id is the KPI's own id, so it survives reordering.
|
||||
List<KpiData> kpis = dataService.kpis();
|
||||
List<KpiData> kpis = dataService.kpis(context.getFilter());
|
||||
for (int i = 0; i < kpis.size(); i++) {
|
||||
KpiData kpi = kpis.get(i);
|
||||
grid.add(new GridStackItem(kpi.id(), i * KPI_WIDTH, 0, KPI_WIDTH, 1,
|
||||
kpiTile(kpi)));
|
||||
KpiTile tile = feed(new KpiTile(getTranslation(kpi.labelKey()), ""), kpi);
|
||||
kpiTiles.put(kpi.id(), tile);
|
||||
grid.add(new GridStackItem(kpi.id(), i * KPI_WIDTH, 0, KPI_WIDTH, 1, tile));
|
||||
}
|
||||
context.addFilterChangeListener(this::updateKpiTiles);
|
||||
|
||||
grid.add(
|
||||
defaultWidget(WidgetRegistry.REVENUE_TREND, 0, 1, 6, 3),
|
||||
@@ -76,7 +93,7 @@ public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
||||
status.setText(getTranslation("gridstack.statusInitial"));
|
||||
status.addClassName("dialect-muted");
|
||||
|
||||
add(toolbar(), grid);
|
||||
add(toolbar(), new DashboardFilterBar(context), grid);
|
||||
}
|
||||
|
||||
private HorizontalLayout toolbar() {
|
||||
@@ -138,18 +155,29 @@ public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
||||
|
||||
private GridStackItem widget(String id, WidgetDefinition definition,
|
||||
int x, int y, int w, int h) {
|
||||
return new GridStackItem(id, x, y, w, h,
|
||||
new Card(getTranslation(definition.titleKey()), definition.factory().get()));
|
||||
return new GridStackItem(id, x, y, w, h, new Card(getTranslation(definition.titleKey()),
|
||||
definition.factory().apply(context)));
|
||||
}
|
||||
|
||||
/** The displayed value comes from the bundle alongside the label, since it
|
||||
* carries locale-specific formatting (decimal separator, currency, "Mio.");
|
||||
* the service supplies the key, not the formatted text. */
|
||||
private KpiTile kpiTile(KpiData kpi) {
|
||||
String label = getTranslation(kpi.labelKey());
|
||||
return new KpiTile(label, getTranslation(kpi.valueKey()))
|
||||
/** Re-feeds the tiles still on the dashboard. A closed tile keeps its entry
|
||||
* in the map — the same KPI can be added back — but is detached, so
|
||||
* feeding it would queue a client call for a chart that is not there. */
|
||||
private void updateKpiTiles(DashboardFilter filter) {
|
||||
for (KpiData kpi : dataService.kpis(filter)) {
|
||||
KpiTile tile = kpiTiles.get(kpi.id());
|
||||
if (tile != null && tile.isAttached()) {
|
||||
feed(tile, kpi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The value's unit and number pattern come from the bundle, since they are
|
||||
* locale-specific (decimal separator, currency, "Mio."); the service
|
||||
* supplies only the number, which depends on the filter. */
|
||||
private KpiTile feed(KpiTile tile, KpiData kpi) {
|
||||
return tile.setValue(getTranslation(kpi.valueKey(), kpi.value()))
|
||||
.setDelta(kpi.deltaPercent())
|
||||
.setSparkline(label, kpi.trend());
|
||||
.setSparkline(tile.getLabel(), kpi.trend());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user