fix: merge GridStackView into DashboardView (#19)
CI / build-and-test (pull_request) Successful in 2m22s
CI / build-and-test (pull_request) Successful in 2m22s
DashboardView and GridStackView rendered the same three charts from the
same hardcoded data, so every chart factory method existed twice. The
dashboard is now the GridStackLayout variant at @Route(""):
- GridStackView deleted, its grid/toolbar/chart factories moved into
DashboardView, which keeps the point-click notifications the old
dashboard had.
- Charts size to their widget (100%/100%) instead of a fixed 400px.
- MainLayout has one dashboard entry; nav.gridstack/page.gridstack keys
dropped from all three bundles.
- Storage key is "dashboard" (was "gridstack-demo"), so a saved layout
from the old route is not reused.
- GridStackViewTest renamed to DashboardViewTest, plus a test asserting
the three charts render.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69
This commit is contained in:
@@ -1,63 +1,135 @@
|
||||
package com.example.views;
|
||||
|
||||
import com.example.components.ApexChart;
|
||||
import com.example.components.BarChart;
|
||||
import com.example.components.Card;
|
||||
import com.example.components.Fa;
|
||||
import com.example.components.GridStackItem;
|
||||
import com.example.components.GridStackLayout;
|
||||
import com.example.components.LineChart;
|
||||
import com.example.components.PieChart;
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
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 java.util.List;
|
||||
|
||||
/**
|
||||
* The dashboard: a {@link GridStackLayout} of draggable/resizable cards whose
|
||||
* layout is persisted per browser, plus controls to add widgets and reset the
|
||||
* layout at runtime. Widgets are removed by their own close button (see
|
||||
* {@link GridStackItem#setClosable(boolean)}), not from the toolbar.
|
||||
*/
|
||||
@Route("")
|
||||
public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
||||
|
||||
private static final String STORAGE_KEY = "dashboard";
|
||||
|
||||
private final GridStackLayout grid = new GridStackLayout();
|
||||
private final Span status = new Span();
|
||||
private int extraWidgetCount;
|
||||
|
||||
public DashboardView() {
|
||||
addClassName("dialect-content");
|
||||
|
||||
List<String> months = List.of(
|
||||
grid.setWidthFull();
|
||||
grid.setStorageKey(STORAGE_KEY);
|
||||
grid.addLayoutChangeListener(e -> status.setText(
|
||||
getTranslation("gridstack.status", e.getPositions().size())));
|
||||
|
||||
grid.add(
|
||||
new GridStackItem("revenue-trend", 0, 0, 6, 3,
|
||||
new Card(getTranslation("card.revenueTrend"), lineChart())),
|
||||
new GridStackItem("revenue-month", 6, 0, 6, 3,
|
||||
new Card(getTranslation("card.revenueByMonth"), barChart())),
|
||||
new GridStackItem("revenue-region", 0, 3, 5, 3,
|
||||
new Card(getTranslation("card.revenueByRegion"), pieChart())),
|
||||
new GridStackItem("hint", 5, 3, 7, 3,
|
||||
new Card(getTranslation("card.gridstackHint"),
|
||||
new Paragraph(getTranslation("gridstack.hint")))));
|
||||
|
||||
status.setText(getTranslation("gridstack.statusInitial"));
|
||||
status.addClassName("dialect-muted");
|
||||
|
||||
add(toolbar(), grid);
|
||||
}
|
||||
|
||||
private HorizontalLayout toolbar() {
|
||||
Button add = new Button(getTranslation("gridstack.addWidget"), Fa.ADD.create(),
|
||||
e -> addWidget());
|
||||
add.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
Button reset = new Button(getTranslation("gridstack.reset"), Fa.RESET.create(),
|
||||
e -> grid.resetLayout());
|
||||
|
||||
HorizontalLayout toolbar = new HorizontalLayout(add, reset, status);
|
||||
toolbar.setPadding(false);
|
||||
toolbar.setWidthFull();
|
||||
toolbar.setAlignItems(Alignment.CENTER);
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
private void addWidget() {
|
||||
// The id must stay stable across reloads for the saved layout to match
|
||||
// it again, so it is derived from a counter rather than a random UUID.
|
||||
// The counter only ever grows: closing a widget must not hand its id to
|
||||
// the next one, or the new widget would inherit the closed one's saved
|
||||
// position.
|
||||
extraWidgetCount++;
|
||||
String title = getTranslation("gridstack.widget", extraWidgetCount);
|
||||
grid.add(new GridStackItem("extra-" + extraWidgetCount, 0, 0, 4, 2,
|
||||
new Card(title, new Paragraph(getTranslation("gridstack.widgetText")))));
|
||||
}
|
||||
|
||||
private Component lineChart() {
|
||||
LineChart chart = new LineChart();
|
||||
chart.setData(getTranslation("chart.revenueSeries"),
|
||||
List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0), months());
|
||||
chart.addPointClickListener(e -> Notification.show(getTranslation(
|
||||
"chart.pointClick", e.getSeriesIndex(), e.getDataPointIndex())));
|
||||
return sizeFull(chart);
|
||||
}
|
||||
|
||||
private Component barChart() {
|
||||
BarChart chart = new BarChart();
|
||||
chart.setData(getTranslation("chart.revenueSeries"),
|
||||
List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0), months());
|
||||
chart.addPointClickListener(e -> Notification.show(getTranslation(
|
||||
"chart.pointClick", e.getSeriesIndex(), e.getDataPointIndex())));
|
||||
return sizeFull(chart);
|
||||
}
|
||||
|
||||
private Component pieChart() {
|
||||
PieChart chart = new PieChart();
|
||||
chart.setData(List.of(30.0, 40.0, 35.0, 50.0),
|
||||
List.of(getTranslation("region.north"), getTranslation("region.south"),
|
||||
getTranslation("region.east"), getTranslation("region.west")));
|
||||
chart.addPointClickListener(e -> Notification.show(
|
||||
getTranslation("chart.sliceClick", e.getDataPointIndex())));
|
||||
return sizeFull(chart);
|
||||
}
|
||||
|
||||
/** Charts fill their grid item instead of using a fixed pixel height, so
|
||||
* resizing a widget resizes the chart (grid-stack.ts fires a window
|
||||
* resize on resizestop, which ApexCharts reflows on). */
|
||||
private Component sizeFull(ApexChart chart) {
|
||||
chart.setWidthFull();
|
||||
chart.setHeight("100%");
|
||||
return chart;
|
||||
}
|
||||
|
||||
private List<String> months() {
|
||||
return List.of(
|
||||
getTranslation("month.jan"), getTranslation("month.feb"),
|
||||
getTranslation("month.mar"), getTranslation("month.apr"),
|
||||
getTranslation("month.may"), getTranslation("month.jun"));
|
||||
|
||||
LineChart lineChart = new LineChart();
|
||||
lineChart.setWidthFull();
|
||||
lineChart.setHeight("400px");
|
||||
lineChart.setData(getTranslation("chart.revenueSeries"),
|
||||
List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0),
|
||||
months);
|
||||
lineChart.addPointClickListener(e -> {
|
||||
Notification.show(getTranslation("chart.pointClick",
|
||||
e.getSeriesIndex(), e.getDataPointIndex()));
|
||||
});
|
||||
|
||||
BarChart barChart = new BarChart();
|
||||
barChart.setWidthFull();
|
||||
barChart.setHeight("400px");
|
||||
barChart.setData(getTranslation("chart.revenueSeries"),
|
||||
List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0),
|
||||
months);
|
||||
barChart.addPointClickListener(e -> {
|
||||
Notification.show(getTranslation("chart.pointClick",
|
||||
e.getSeriesIndex(), e.getDataPointIndex()));
|
||||
});
|
||||
|
||||
PieChart pieChart = new PieChart();
|
||||
pieChart.setWidthFull();
|
||||
pieChart.setHeight("400px");
|
||||
pieChart.setData(
|
||||
List.of(30.0, 40.0, 35.0, 50.0),
|
||||
List.of(getTranslation("region.north"), getTranslation("region.south"),
|
||||
getTranslation("region.east"), getTranslation("region.west")));
|
||||
pieChart.addPointClickListener(e -> {
|
||||
Notification.show(getTranslation("chart.sliceClick", e.getDataPointIndex()));
|
||||
});
|
||||
|
||||
add(new Card(getTranslation("card.revenueTrend"), lineChart),
|
||||
new Card(getTranslation("card.revenueByMonth"), barChart),
|
||||
new Card(getTranslation("card.revenueByRegion"), pieChart));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
package com.example.views;
|
||||
|
||||
import com.example.components.BarChart;
|
||||
import com.example.components.Card;
|
||||
import com.example.components.Fa;
|
||||
import com.example.components.GridStackItem;
|
||||
import com.example.components.GridStackLayout;
|
||||
import com.example.components.LineChart;
|
||||
import com.example.components.PieChart;
|
||||
import com.example.components.ApexChart;
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
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 java.util.List;
|
||||
|
||||
/**
|
||||
* Showcase for {@link GridStackLayout}: a dashboard of draggable/resizable
|
||||
* cards whose layout is persisted per browser, plus controls to add widgets and
|
||||
* reset the layout at runtime. Widgets are removed by their own close button
|
||||
* (see {@link GridStackItem#setClosable(boolean)}), not from the toolbar.
|
||||
*/
|
||||
@Route("gridstack")
|
||||
public class GridStackView extends VerticalLayout implements HasDynamicTitle {
|
||||
|
||||
private static final String STORAGE_KEY = "gridstack-demo";
|
||||
|
||||
private final GridStackLayout grid = new GridStackLayout();
|
||||
private final Span status = new Span();
|
||||
private int extraWidgetCount;
|
||||
|
||||
public GridStackView() {
|
||||
addClassName("dialect-content");
|
||||
|
||||
grid.setWidthFull();
|
||||
grid.setStorageKey(STORAGE_KEY);
|
||||
grid.addLayoutChangeListener(e -> status.setText(
|
||||
getTranslation("gridstack.status", e.getPositions().size())));
|
||||
|
||||
grid.add(
|
||||
new GridStackItem("revenue-trend", 0, 0, 6, 3,
|
||||
new Card(getTranslation("card.revenueTrend"), lineChart())),
|
||||
new GridStackItem("revenue-month", 6, 0, 6, 3,
|
||||
new Card(getTranslation("card.revenueByMonth"), barChart())),
|
||||
new GridStackItem("revenue-region", 0, 3, 5, 3,
|
||||
new Card(getTranslation("card.revenueByRegion"), pieChart())),
|
||||
new GridStackItem("hint", 5, 3, 7, 3,
|
||||
new Card(getTranslation("card.gridstackHint"),
|
||||
new Paragraph(getTranslation("gridstack.hint")))));
|
||||
|
||||
status.setText(getTranslation("gridstack.statusInitial"));
|
||||
status.addClassName("dialect-muted");
|
||||
|
||||
add(toolbar(), grid);
|
||||
}
|
||||
|
||||
private HorizontalLayout toolbar() {
|
||||
Button add = new Button(getTranslation("gridstack.addWidget"), Fa.ADD.create(),
|
||||
e -> addWidget());
|
||||
add.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
Button reset = new Button(getTranslation("gridstack.reset"), Fa.RESET.create(),
|
||||
e -> grid.resetLayout());
|
||||
|
||||
HorizontalLayout toolbar = new HorizontalLayout(add, reset, status);
|
||||
toolbar.setPadding(false);
|
||||
toolbar.setWidthFull();
|
||||
toolbar.setAlignItems(Alignment.CENTER);
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
private void addWidget() {
|
||||
// The id must stay stable across reloads for the saved layout to match
|
||||
// it again, so it is derived from a counter rather than a random UUID.
|
||||
// The counter only ever grows: closing a widget must not hand its id to
|
||||
// the next one, or the new widget would inherit the closed one's saved
|
||||
// position.
|
||||
extraWidgetCount++;
|
||||
String title = getTranslation("gridstack.widget", extraWidgetCount);
|
||||
grid.add(new GridStackItem("extra-" + extraWidgetCount, 0, 0, 4, 2,
|
||||
new Card(title, new Paragraph(getTranslation("gridstack.widgetText")))));
|
||||
}
|
||||
|
||||
private Component lineChart() {
|
||||
LineChart chart = new LineChart();
|
||||
chart.setData(getTranslation("chart.revenueSeries"),
|
||||
List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0), months());
|
||||
return sizeFull(chart);
|
||||
}
|
||||
|
||||
private Component barChart() {
|
||||
BarChart chart = new BarChart();
|
||||
chart.setData(getTranslation("chart.revenueSeries"),
|
||||
List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0), months());
|
||||
return sizeFull(chart);
|
||||
}
|
||||
|
||||
private Component pieChart() {
|
||||
PieChart chart = new PieChart();
|
||||
chart.setData(List.of(30.0, 40.0, 35.0, 50.0),
|
||||
List.of(getTranslation("region.north"), getTranslation("region.south"),
|
||||
getTranslation("region.east"), getTranslation("region.west")));
|
||||
return sizeFull(chart);
|
||||
}
|
||||
|
||||
/** Charts fill their grid item instead of using a fixed pixel height, so
|
||||
* resizing a widget resizes the chart (grid-stack.ts fires a window
|
||||
* resize on resizestop, which ApexCharts reflows on). */
|
||||
private Component sizeFull(ApexChart chart) {
|
||||
chart.setWidthFull();
|
||||
chart.setHeight("100%");
|
||||
return chart;
|
||||
}
|
||||
|
||||
private List<String> months() {
|
||||
return List.of(
|
||||
getTranslation("month.jan"), getTranslation("month.feb"),
|
||||
getTranslation("month.mar"), getTranslation("month.apr"),
|
||||
getTranslation("month.may"), getTranslation("month.jun"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPageTitle() {
|
||||
return getTranslation("page.gridstack");
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,6 @@ public class MainLayout extends AppLayout {
|
||||
Fa.FORM.create()));
|
||||
nav.addItem(new SideNavItem(getTranslation("nav.table"), TableView.class,
|
||||
Fa.TABLE.create()));
|
||||
nav.addItem(new SideNavItem(getTranslation("nav.gridstack"), GridStackView.class,
|
||||
Fa.GRID.create()));
|
||||
|
||||
addToDrawer(nav);
|
||||
}
|
||||
|
||||
@@ -10,12 +10,10 @@ lang.es=Español
|
||||
nav.dashboard=Dashboard
|
||||
nav.form=Formular
|
||||
nav.table=Tabelle
|
||||
nav.gridstack=Raster
|
||||
|
||||
page.dashboard=Dashboard
|
||||
page.form=Formular
|
||||
page.table=Tabelle
|
||||
page.gridstack=Raster-Layout
|
||||
|
||||
card.revenueTrend=Umsatz-Entwicklung
|
||||
card.revenueByMonth=Umsatz nach Monat
|
||||
|
||||
@@ -10,12 +10,10 @@ lang.es=Español
|
||||
nav.dashboard=Dashboard
|
||||
nav.form=Form
|
||||
nav.table=Table
|
||||
nav.gridstack=Grid
|
||||
|
||||
page.dashboard=Dashboard
|
||||
page.form=Form
|
||||
page.table=Table
|
||||
page.gridstack=Grid layout
|
||||
|
||||
card.revenueTrend=Revenue Trend
|
||||
card.revenueByMonth=Revenue by Month
|
||||
|
||||
@@ -10,12 +10,10 @@ lang.es=Español
|
||||
nav.dashboard=Panel
|
||||
nav.form=Formulario
|
||||
nav.table=Tabla
|
||||
nav.gridstack=Cuadrícula
|
||||
|
||||
page.dashboard=Panel
|
||||
page.form=Formulario
|
||||
page.table=Tabla
|
||||
page.gridstack=Diseño de cuadrícula
|
||||
|
||||
card.revenueTrend=Evolución de ingresos
|
||||
card.revenueByMonth=Ingresos por mes
|
||||
|
||||
+19
-7
@@ -1,8 +1,11 @@
|
||||
package com.example.views;
|
||||
|
||||
import com.example.Application;
|
||||
import com.example.components.BarChart;
|
||||
import com.example.components.GridStackItem;
|
||||
import com.example.components.GridStackLayout;
|
||||
import com.example.components.LineChart;
|
||||
import com.example.components.PieChart;
|
||||
import com.vaadin.browserless.SpringBrowserlessTest;
|
||||
import com.vaadin.browserless.ViewPackages;
|
||||
import com.vaadin.browserless.internal.ElementUtilsKt;
|
||||
@@ -21,12 +24,12 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@SpringBootTest(classes = Application.class)
|
||||
@ViewPackages(classes = GridStackView.class)
|
||||
class GridStackViewTest extends SpringBrowserlessTest {
|
||||
@ViewPackages(classes = DashboardView.class)
|
||||
class DashboardViewTest extends SpringBrowserlessTest {
|
||||
|
||||
@Test
|
||||
void view_rendersGridWithDefaultWidgets() {
|
||||
navigate(GridStackView.class);
|
||||
navigate(DashboardView.class);
|
||||
|
||||
GridStackLayout grid = $view(GridStackLayout.class).first();
|
||||
assertNotNull(grid);
|
||||
@@ -36,9 +39,18 @@ class GridStackViewTest extends SpringBrowserlessTest {
|
||||
assertTrue(layout.stream().allMatch(p -> p.w() > 0 && p.h() > 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void view_rendersTheThreeCharts() {
|
||||
navigate(DashboardView.class);
|
||||
|
||||
assertNotNull($view(LineChart.class).first());
|
||||
assertNotNull($view(BarChart.class).first());
|
||||
assertNotNull($view(PieChart.class).first());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addAndCloseWidget_changesItemCount() {
|
||||
navigate(GridStackView.class);
|
||||
navigate(DashboardView.class);
|
||||
|
||||
GridStackLayout grid = $view(GridStackLayout.class).first();
|
||||
int initial = grid.getLayout().size();
|
||||
@@ -52,7 +64,7 @@ class GridStackViewTest extends SpringBrowserlessTest {
|
||||
|
||||
@Test
|
||||
void closeButton_removesTheClickedWidgetOnly() {
|
||||
navigate(GridStackView.class);
|
||||
navigate(DashboardView.class);
|
||||
|
||||
GridStackLayout grid = $view(GridStackLayout.class).first();
|
||||
GridStackItem first = $view(GridStackItem.class).first();
|
||||
@@ -68,7 +80,7 @@ class GridStackViewTest extends SpringBrowserlessTest {
|
||||
|
||||
@Test
|
||||
void closeListener_firesOnceItemIsDetached() {
|
||||
navigate(GridStackView.class);
|
||||
navigate(DashboardView.class);
|
||||
|
||||
GridStackLayout grid = $view(GridStackLayout.class).first();
|
||||
GridStackItem item = $view(GridStackItem.class).first();
|
||||
@@ -83,7 +95,7 @@ class GridStackViewTest extends SpringBrowserlessTest {
|
||||
|
||||
@Test
|
||||
void nonClosableItem_hasNoCloseButton() {
|
||||
navigate(GridStackView.class);
|
||||
navigate(DashboardView.class);
|
||||
|
||||
GridStackItem item = $view(GridStackItem.class).first();
|
||||
assertTrue(item.isClosable(), "items are closable by default");
|
||||
Reference in New Issue
Block a user