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:
@@ -7,13 +7,16 @@ import com.example.components.LineChart;
|
||||
import com.example.components.PieChart;
|
||||
import com.example.data.ChartDataService;
|
||||
import com.example.data.ChartSeries;
|
||||
import com.example.data.DashboardFilter;
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.shared.Registration;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* The widget types a dashboard can show. Holding them here rather than inline in
|
||||
@@ -21,9 +24,13 @@ import java.util.Map;
|
||||
* closed widget be brought back: the view only decides <em>where</em> a widget
|
||||
* sits, the registry decides what one <em>is</em>.
|
||||
* <p>
|
||||
* Numbers come from {@link ChartDataService}; the factories resolve the data's
|
||||
* translation keys against the bundle through the chart component itself, so
|
||||
* they follow the current UI locale without the registry being a component.
|
||||
* Numbers come from {@link ChartDataService}, queried with the filter the
|
||||
* widget's {@link DashboardContext} currently holds; the factories resolve the
|
||||
* data's translation keys against the bundle through the chart component
|
||||
* itself, so they follow the current UI locale without the registry being a
|
||||
* component. Every widget also subscribes to the context, so a filter change
|
||||
* re-feeds it — in place, through {@code updateData}, rather than by rebuilding
|
||||
* the chart.
|
||||
*/
|
||||
@Service
|
||||
public class WidgetRegistry {
|
||||
@@ -40,9 +47,9 @@ public class WidgetRegistry {
|
||||
this.dataService = dataService;
|
||||
|
||||
register(new WidgetDefinition(REVENUE_TREND, "card.revenueTrend", 6, 3,
|
||||
() -> axisChart(new LineChart())));
|
||||
context -> axisChart(new LineChart(), context)));
|
||||
register(new WidgetDefinition(REVENUE_MONTH, "card.revenueByMonth", 6, 3,
|
||||
() -> axisChart(new BarChart())));
|
||||
context -> axisChart(new BarChart(), context)));
|
||||
register(new WidgetDefinition(REVENUE_REGION, "card.revenueByRegion", 5, 3,
|
||||
this::pieChart));
|
||||
}
|
||||
@@ -66,22 +73,40 @@ public class WidgetRegistry {
|
||||
return definition;
|
||||
}
|
||||
|
||||
private Component axisChart(AxisChart chart) {
|
||||
ChartSeries series = dataService.revenueByMonth();
|
||||
chart.setData(chart.getTranslation(series.nameKey()), series.values(),
|
||||
translate(chart, series.categoryKeys()));
|
||||
private Component axisChart(AxisChart chart, DashboardContext context) {
|
||||
chart.addPointClickListener(e -> Notification.show(chart.getTranslation(
|
||||
"chart.pointClick", e.getSeriesIndex(), e.getDataPointIndex())));
|
||||
return sizeFull(chart);
|
||||
return bind(chart, context, filter -> {
|
||||
ChartSeries series = dataService.revenueByMonth(filter);
|
||||
chart.updateData(chart.getTranslation(series.nameKey()), series.values(),
|
||||
translate(chart, series.categoryKeys()));
|
||||
});
|
||||
}
|
||||
|
||||
private Component pieChart() {
|
||||
ChartSeries series = dataService.revenueByRegion();
|
||||
private Component pieChart(DashboardContext context) {
|
||||
PieChart chart = new PieChart();
|
||||
// A pie has no series name — its categories are the slice labels.
|
||||
chart.setData(series.values(), translate(chart, series.categoryKeys()));
|
||||
chart.addPointClickListener(e -> Notification.show(
|
||||
chart.getTranslation("chart.sliceClick", e.getDataPointIndex())));
|
||||
return bind(chart, context, filter -> {
|
||||
ChartSeries series = dataService.revenueByRegion(filter);
|
||||
// A pie has no series name — its categories are the slice labels.
|
||||
chart.updateData(series.values(), translate(chart, series.categoryKeys()));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Feeds the chart from the current filter and keeps it fed: the
|
||||
* subscription is dropped when the widget is detached (closed, or the
|
||||
* dashboard left), so a chart that is gone is not updated any more.
|
||||
* <p>
|
||||
* The first feed goes through the same {@code updateData} path as later
|
||||
* ones, which falls back to a full render while nothing has been drawn yet.
|
||||
*/
|
||||
private Component bind(ApexChart chart, DashboardContext context,
|
||||
Consumer<DashboardFilter> feed) {
|
||||
feed.accept(context.getFilter());
|
||||
Registration registration = context.addFilterChangeListener(feed);
|
||||
chart.addDetachListener(e -> registration.remove());
|
||||
return sizeFull(chart);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user