fix: export widget data as CSV from the action menu (#28)
CI / build-and-test (pull_request) Successful in 2m26s
CI / build-and-test (pull_request) Successful in 2m26s
The ApexCharts toolbar stays hidden (DialectTheme), so the dashboard had no
export at all. Adds an "Als CSV exportieren" entry to the per-widget action
menu, served server-side from the same data the widget renders.
- CsvExport: the export table plus its CSV dialect (';' separator, CRLF,
UTF-8 BOM, locale-formatted numbers) and file-name slugging.
- GridStackItem.setActionDownload: turns a menu entry's caption into an
anchor over a DownloadHandler, so an entry can hand out a file.
- WidgetRegistry: keeps each widget's query, not only its result, so the
Export hook re-runs it — an export always matches the current filter.
- DashboardView: builds the download on click; file name is widget title +
period (revenue-trend-half-year.csv).
Chart image export is left as the follow-up the issue calls optional.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69
This commit is contained in:
@@ -9,6 +9,7 @@ import com.example.components.KpiTile;
|
||||
import com.example.data.ChartDataService;
|
||||
import com.example.data.DashboardFilter;
|
||||
import com.example.data.KpiData;
|
||||
import com.example.export.CsvExport;
|
||||
import com.example.widgets.DashboardContext;
|
||||
import com.example.widgets.WidgetDefinition;
|
||||
import com.example.widgets.WidgetRegistry;
|
||||
@@ -22,10 +23,17 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.HasDynamicTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.server.HttpStatusCode;
|
||||
import com.vaadin.flow.server.VaadinSession;
|
||||
import com.vaadin.flow.server.streams.DownloadHandler;
|
||||
import com.vaadin.flow.server.streams.DownloadResponse;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* The dashboard: a {@link GridStackLayout} of draggable/resizable cards whose
|
||||
@@ -172,7 +180,9 @@ public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
||||
Component content = definition.factory().apply(context);
|
||||
GridStackItem item = new GridStackItem(id, x, y, w, h,
|
||||
new Card(getTranslation(definition.titleKey()), content));
|
||||
item.setActions(Action.REFRESH, Action.MAXIMIZE, Action.DUPLICATE, Action.REMOVE);
|
||||
item.setActions(Action.REFRESH, Action.MAXIMIZE, Action.DUPLICATE,
|
||||
Action.EXPORT, Action.REMOVE);
|
||||
item.setActionDownload(Action.EXPORT, csvDownload(definition, content));
|
||||
item.addActionListener(e -> {
|
||||
switch (e.getAction()) {
|
||||
case REFRESH -> WidgetRegistry.refresh(content);
|
||||
@@ -183,6 +193,41 @@ public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* The widget's data as a CSV attachment. Nothing is computed here: the
|
||||
* callback runs when the user picks the entry, so data, labels and file
|
||||
* name are all of the moment — including whatever the filter bar is set to
|
||||
* then.
|
||||
* <p>
|
||||
* A download is served on a request of its own, outside the session lock
|
||||
* and without a current {@code UI} (see
|
||||
* {@code StreamRequestHandler#callElementResourceHandler}), so the lock is
|
||||
* taken for the read and the locale is passed explicitly.
|
||||
*/
|
||||
private DownloadHandler csvDownload(WidgetDefinition definition, Component content) {
|
||||
return DownloadHandler.fromInputStream(event -> {
|
||||
VaadinSession session = event.getSession();
|
||||
session.lock();
|
||||
Locale locale;
|
||||
String fileName;
|
||||
Optional<CsvExport> table;
|
||||
try {
|
||||
locale = event.getUI().getLocale();
|
||||
table = WidgetRegistry.export(content, locale);
|
||||
fileName = CsvExport.fileName(getTranslation(locale, definition.titleKey()),
|
||||
getTranslation(locale, context.getFilter().period().labelKey()));
|
||||
} finally {
|
||||
session.unlock();
|
||||
}
|
||||
if (table.isEmpty()) {
|
||||
return DownloadResponse.error(HttpStatusCode.NOT_FOUND);
|
||||
}
|
||||
byte[] csv = table.get().toBytes(locale);
|
||||
return new DownloadResponse(new ByteArrayInputStream(csv), fileName,
|
||||
"text/csv;charset=utf-8", csv.length);
|
||||
});
|
||||
}
|
||||
|
||||
/** Re-feeds a single tile from the current filter — the action menu's
|
||||
* refresh, which asks for one widget, not for the dashboard. */
|
||||
private void refreshKpiTile(String kpiId) {
|
||||
|
||||
Reference in New Issue
Block a user