Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 505ad32d4a | |||
| dbe304e61e |
@@ -17,6 +17,7 @@ import com.vaadin.flow.component.Component;
|
|||||||
import com.vaadin.flow.component.button.Button;
|
import com.vaadin.flow.component.button.Button;
|
||||||
import com.vaadin.flow.component.button.ButtonVariant;
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
import com.vaadin.flow.component.dialog.Dialog;
|
import com.vaadin.flow.component.dialog.Dialog;
|
||||||
|
import com.vaadin.flow.component.html.Div;
|
||||||
import com.vaadin.flow.component.html.Paragraph;
|
import com.vaadin.flow.component.html.Paragraph;
|
||||||
import com.vaadin.flow.component.html.Span;
|
import com.vaadin.flow.component.html.Span;
|
||||||
import com.vaadin.flow.component.notification.Notification;
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
@@ -66,6 +67,10 @@ public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
private final DashboardContext context = new DashboardContext();
|
private final DashboardContext context = new DashboardContext();
|
||||||
private final GridStackLayout grid = new GridStackLayout();
|
private final GridStackLayout grid = new GridStackLayout();
|
||||||
private final Span status = new Span();
|
private final Span status = new Span();
|
||||||
|
/** Shown instead of {@link #grid} once every widget has been closed; not a
|
||||||
|
* {@link GridStackItem} itself, so it never becomes draggable and never
|
||||||
|
* shows up in {@link GridStackLayout#getLayout()}. */
|
||||||
|
private final Div emptyState = new Div();
|
||||||
/** The KPI tiles by KPI id, so a filter change re-feeds each tile with the
|
/** 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. */
|
* data of the same KPI rather than by position. */
|
||||||
private final Map<String, KpiTile> kpiTiles = new LinkedHashMap<>();
|
private final Map<String, KpiTile> kpiTiles = new LinkedHashMap<>();
|
||||||
@@ -81,10 +86,24 @@ public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
grid.addLayoutChangeListener(e -> status.setText(
|
grid.addLayoutChangeListener(e -> status.setText(
|
||||||
getTranslation("gridstack.status", e.getPositions().size())));
|
getTranslation("gridstack.status", e.getPositions().size())));
|
||||||
|
|
||||||
// KPI tiles first: the numbers a dashboard is read for, above the charts
|
configureEmptyState();
|
||||||
// that explain them. They are 3x1 — a quarter row each, one cell high,
|
buildDefaultWidgets();
|
||||||
// laid out left to right in the order the service returns them. The
|
context.addFilterChangeListener(this::updateKpiTiles);
|
||||||
// grid id is the KPI's own id, so it survives reordering.
|
|
||||||
|
status.setText(getTranslation("gridstack.statusInitial"));
|
||||||
|
status.addClassName("dialect-muted");
|
||||||
|
|
||||||
|
add(toolbar(), new DashboardFilterBar(context), grid, emptyState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The dashboard's initial widget set: the KPI tiles first — the numbers a
|
||||||
|
* dashboard is read for, above the charts that explain them — then the
|
||||||
|
* three default charts and the usage hint. Also used to rebuild the
|
||||||
|
* dashboard from scratch via {@link #restoreDefaultWidgets()}. */
|
||||||
|
private void buildDefaultWidgets() {
|
||||||
|
// 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(context.getFilter());
|
List<KpiData> kpis = dataService.kpis(context.getFilter());
|
||||||
for (int i = 0; i < kpis.size(); i++) {
|
for (int i = 0; i < kpis.size(); i++) {
|
||||||
KpiData kpi = kpis.get(i);
|
KpiData kpi = kpis.get(i);
|
||||||
@@ -101,7 +120,6 @@ public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
});
|
});
|
||||||
addWidgetToGrid(item);
|
addWidgetToGrid(item);
|
||||||
}
|
}
|
||||||
context.addFilterChangeListener(this::updateKpiTiles);
|
|
||||||
|
|
||||||
List.of(
|
List.of(
|
||||||
defaultWidget(WidgetRegistry.REVENUE_TREND, 0, 1, 6, 3),
|
defaultWidget(WidgetRegistry.REVENUE_TREND, 0, 1, 6, 3),
|
||||||
@@ -111,11 +129,50 @@ public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
new Card(getTranslation("card.gridstackHint"),
|
new Card(getTranslation("card.gridstackHint"),
|
||||||
new Paragraph(getTranslation("gridstack.hint")))))
|
new Paragraph(getTranslation("gridstack.hint")))))
|
||||||
.forEach(this::addWidgetToGrid);
|
.forEach(this::addWidgetToGrid);
|
||||||
|
}
|
||||||
|
|
||||||
status.setText(getTranslation("gridstack.statusInitial"));
|
/** Builds the placeholder shown once every widget has been closed: a short
|
||||||
status.addClassName("dialect-muted");
|
* explanation, a CTA that opens the same {@link #openWidgetPicker() widget
|
||||||
|
* picker} as the toolbar, and a secondary action that rebuilds the default
|
||||||
|
* layout — {@link GridStackLayout#resetLayout()} alone cannot do that here,
|
||||||
|
* since it only repositions widgets still present, and none are left. */
|
||||||
|
private void configureEmptyState() {
|
||||||
|
emptyState.addClassName("dialect-empty-state");
|
||||||
|
|
||||||
add(toolbar(), new DashboardFilterBar(context), grid);
|
var icon = Fa.GRID.create();
|
||||||
|
icon.addClassName("dialect-empty-state__icon");
|
||||||
|
|
||||||
|
Span title = new Span(getTranslation("gridstack.emptyTitle"));
|
||||||
|
title.addClassName("dialect-empty-state__title");
|
||||||
|
Span hint = new Span(getTranslation("gridstack.emptyHint"));
|
||||||
|
hint.addClassName("dialect-muted");
|
||||||
|
|
||||||
|
Button add = new Button(getTranslation("gridstack.addWidget"), Fa.ADD.create(),
|
||||||
|
e -> openWidgetPicker());
|
||||||
|
add.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
Button restore = new Button(getTranslation("gridstack.restoreDefaults"), Fa.RESET.create(),
|
||||||
|
e -> restoreDefaultWidgets());
|
||||||
|
|
||||||
|
HorizontalLayout actions = new HorizontalLayout(add, restore);
|
||||||
|
actions.addClassName("dialect-empty-state__actions");
|
||||||
|
|
||||||
|
emptyState.add(icon, title, hint, actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clears the stale KPI tile references and rebuilds the initial widget set
|
||||||
|
* — the empty state's secondary action. */
|
||||||
|
private void restoreDefaultWidgets() {
|
||||||
|
kpiTiles.clear();
|
||||||
|
buildDefaultWidgets();
|
||||||
|
grid.resetLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Toggles {@link #grid} and {@link #emptyState} based on whether any
|
||||||
|
* widget is left — called from every path that adds or removes one. */
|
||||||
|
private void updateEmptyState() {
|
||||||
|
boolean empty = grid.getLayout().isEmpty();
|
||||||
|
emptyState.setVisible(empty);
|
||||||
|
grid.setVisible(!empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
private HorizontalLayout toolbar() {
|
private HorizontalLayout toolbar() {
|
||||||
@@ -174,6 +231,7 @@ public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
private void addWidgetToGrid(GridStackItem item) {
|
private void addWidgetToGrid(GridStackItem item) {
|
||||||
item.addCloseListener(this::offerUndo);
|
item.addCloseListener(this::offerUndo);
|
||||||
grid.add(item);
|
grid.add(item);
|
||||||
|
updateEmptyState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,6 +248,7 @@ public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
* in the grid's next free slot.
|
* in the grid's next free slot.
|
||||||
*/
|
*/
|
||||||
private void offerUndo(GridStackItem.CloseEvent event) {
|
private void offerUndo(GridStackItem.CloseEvent event) {
|
||||||
|
updateEmptyState();
|
||||||
if (!event.isFromClient()) {
|
if (!event.isFromClient()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -203,6 +262,7 @@ public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
Button undo = new Button(getTranslation("gridstack.undo"), e -> {
|
Button undo = new Button(getTranslation("gridstack.undo"), e -> {
|
||||||
item.setPosition(position.x(), position.y(), position.w(), position.h());
|
item.setPosition(position.x(), position.y(), position.w(), position.h());
|
||||||
grid.add(item);
|
grid.add(item);
|
||||||
|
updateEmptyState();
|
||||||
toast.close();
|
toast.close();
|
||||||
});
|
});
|
||||||
undo.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE);
|
undo.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE);
|
||||||
|
|||||||
@@ -419,4 +419,33 @@ apex-chart.dialect-sparkline .apexcharts-xaxis {
|
|||||||
background: var(--dialect-bg);
|
background: var(--dialect-bg);
|
||||||
border: 2px dashed var(--dialect-border);
|
border: 2px dashed var(--dialect-border);
|
||||||
border-radius: var(--dialect-radius);
|
border-radius: var(--dialect-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DashboardView's placeholder for a widget-less dashboard (not a
|
||||||
|
GridStackItem, see DashboardView.emptyState) — dashed like gridstack's own
|
||||||
|
drop placeholder above, to read as "nothing here yet" rather than a card. */
|
||||||
|
.dialect-empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 48px 24px;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px dashed var(--dialect-border);
|
||||||
|
border-radius: var(--dialect-radius);
|
||||||
|
color: var(--dialect-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialect-empty-state__icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: var(--dialect-primary);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialect-empty-state__title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialect-empty-state__actions {
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
@@ -39,6 +39,9 @@ gridstack.remove=Entfernen
|
|||||||
gridstack.export=Als CSV exportieren
|
gridstack.export=Als CSV exportieren
|
||||||
gridstack.closed=Widget entfernt
|
gridstack.closed=Widget entfernt
|
||||||
gridstack.undo=Rückgängig
|
gridstack.undo=Rückgängig
|
||||||
|
gridstack.emptyTitle=Keine Widgets auf dem Dashboard
|
||||||
|
gridstack.emptyHint=Alle Widgets wurden geschlossen. Füge eines hinzu oder stelle das Standardlayout wieder her.
|
||||||
|
gridstack.restoreDefaults=Standardlayout wiederherstellen
|
||||||
|
|
||||||
# Spaltenüberschrift der CSV-Exporte; die Wertspalte trägt den Serien-Namen.
|
# Spaltenüberschrift der CSV-Exporte; die Wertspalte trägt den Serien-Namen.
|
||||||
export.category=Kategorie
|
export.category=Kategorie
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ gridstack.remove=Remove
|
|||||||
gridstack.export=Export as CSV
|
gridstack.export=Export as CSV
|
||||||
gridstack.closed=Widget removed
|
gridstack.closed=Widget removed
|
||||||
gridstack.undo=Undo
|
gridstack.undo=Undo
|
||||||
|
gridstack.emptyTitle=No widgets on the dashboard
|
||||||
|
gridstack.emptyHint=All widgets have been closed. Add one or restore the default layout.
|
||||||
|
gridstack.restoreDefaults=Restore default layout
|
||||||
|
|
||||||
# Column header of the CSV exports; the value column carries the series name.
|
# Column header of the CSV exports; the value column carries the series name.
|
||||||
export.category=Category
|
export.category=Category
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ gridstack.remove=Eliminar
|
|||||||
gridstack.export=Exportar como CSV
|
gridstack.export=Exportar como CSV
|
||||||
gridstack.closed=Widget eliminado
|
gridstack.closed=Widget eliminado
|
||||||
gridstack.undo=Deshacer
|
gridstack.undo=Deshacer
|
||||||
|
gridstack.emptyTitle=No hay widgets en el panel
|
||||||
|
gridstack.emptyHint=Se han cerrado todos los widgets. Añade uno o restablece el diseño predeterminado.
|
||||||
|
gridstack.restoreDefaults=Restablecer diseño predeterminado
|
||||||
|
|
||||||
# Encabezado de columna de las exportaciones CSV; la columna de valores lleva
|
# Encabezado de columna de las exportaciones CSV; la columna de valores lleva
|
||||||
# el nombre de la serie.
|
# el nombre de la serie.
|
||||||
|
|||||||
@@ -328,6 +328,54 @@ class DashboardViewTest extends SpringBrowserlessTest {
|
|||||||
.withText(translate("gridstack.undo")).first().click();
|
.withText(translate("gridstack.undo")).first().click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void closingEveryWidget_showsEmptyStateAndHidesTheGrid() {
|
||||||
|
navigate(DashboardView.class);
|
||||||
|
GridStackLayout grid = $view(GridStackLayout.class).first();
|
||||||
|
|
||||||
|
closeAllWidgets();
|
||||||
|
|
||||||
|
assertTrue(grid.getLayout().isEmpty(), "no widget should be left");
|
||||||
|
assertFalse(grid.isVisible(), "the empty grid must not show as an empty box");
|
||||||
|
Div emptyState = $view(Div.class).withClassName("dialect-empty-state").first();
|
||||||
|
assertTrue(emptyState.isVisible(), "the empty state must appear");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addingAWidget_hidesTheEmptyStateAgain() {
|
||||||
|
navigate(DashboardView.class);
|
||||||
|
closeAllWidgets();
|
||||||
|
|
||||||
|
button("gridstack.addWidget").click();
|
||||||
|
pickWidget("card.revenueByRegion");
|
||||||
|
|
||||||
|
assertFalse($view(Div.class).withClassName("dialect-empty-state").exists(),
|
||||||
|
"the empty state must be gone once a widget is back — invisible components drop out of the query");
|
||||||
|
assertTrue($view(GridStackLayout.class).first().isVisible());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void restoreDefaultLayout_rebuildsTheDefaultWidgetsFromTheEmptyState() {
|
||||||
|
navigate(DashboardView.class);
|
||||||
|
closeAllWidgets();
|
||||||
|
|
||||||
|
button("gridstack.restoreDefaults").click();
|
||||||
|
|
||||||
|
GridStackLayout grid = $view(GridStackLayout.class).first();
|
||||||
|
assertEquals(DEFAULT_WIDGETS, grid.getLayout().size());
|
||||||
|
assertFalse($view(Div.class).withClassName("dialect-empty-state").exists(),
|
||||||
|
"the empty state must be gone once the default layout is back");
|
||||||
|
assertTrue(grid.isVisible());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeAllWidgets() {
|
||||||
|
List<GridStackItem> items = $view(GridStackItem.class).all();
|
||||||
|
while (!items.isEmpty()) {
|
||||||
|
clickCloseButton(items.getFirst());
|
||||||
|
items = $view(GridStackItem.class).all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void nonClosableItem_hasNoCloseButton() {
|
void nonClosableItem_hasNoCloseButton() {
|
||||||
navigate(DashboardView.class);
|
navigate(DashboardView.class);
|
||||||
|
|||||||
Reference in New Issue
Block a user