DashboardEditable.java
package com.vaadin.demo.component.dashboard;
import com.vaadin.flow.component.contextmenu.MenuItem;
import com.vaadin.flow.component.dashboard.Dashboard;
import com.vaadin.flow.component.dashboard.DashboardWidget;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.menubar.MenuBar;
import com.vaadin.flow.component.menubar.MenuBarVariant;
import com.vaadin.flow.router.Route;
import java.util.List;
@Route("dashboard-editable")
public class DashboardEditable extends Div {
private final DashboardStorage dashboardStorage;
private Dashboard dashboard;
// tag::snippet[]
// NOTE: This example uses the additional classes WidgetConfig and
// DashboardStorage, which you can find by switching to the respective file
// tab.
// Since the default DashboardWidget class doesn't allow setting custom
// data, we create a custom class that extends DashboardWidget, and add a
// field for storing the widget type.
public static class CustomWidget extends DashboardWidget {
private final WidgetConfig.WidgetType type;
public CustomWidget(WidgetConfig.WidgetType type, String title) {
super(title);
this.type = type;
}
public WidgetConfig.WidgetType getType() {
return type;
}
}
// This is the default configuration for the dashboard. Note that the order
// of the widgets in the list determines the order in which they are
// displayed in the dashboard.
private final List<WidgetConfig> defaultConfig = List.of(
new WidgetConfig(WidgetConfig.WidgetType.VISITORS, 1, 1),
new WidgetConfig(WidgetConfig.WidgetType.DOWNLOADS, 1, 1),
new WidgetConfig(WidgetConfig.WidgetType.CONVERSIONS, 1, 1),
new WidgetConfig(WidgetConfig.WidgetType.VISITORS_BY_COUNTRY, 1, 2),
new WidgetConfig(WidgetConfig.WidgetType.BROWSER_DISTRIBUTION, 1,
1),
new WidgetConfig(WidgetConfig.WidgetType.CAT_IMAGE, 1, 1),
new WidgetConfig(WidgetConfig.WidgetType.VISITORS_BY_BROWSER, 2,
1));
public DashboardEditable(DashboardStorage dashboardStorage) {
this.dashboardStorage = dashboardStorage;
createToolbar();
createDashboard();
}
private void createDashboard() {
// Create dashboard and load initial configuration
dashboard = new Dashboard();
loadConfiguration();
dashboard.setMinimumColumnWidth("150px");
dashboard.setMaximumColumnCount(3);
add(dashboard);
}
private void createToolbar() {
MenuBar toolbar = new MenuBar();
toolbar.addThemeVariants(MenuBarVariant.LUMO_DROPDOWN_INDICATORS);
MenuItem edit = toolbar.addItem("Edit");
edit.addThemeNames("primary");
edit.addClickListener(event -> {
if (dashboard.isEditable()) {
dashboard.setEditable(false);
edit.setText("Edit");
} else {
dashboard.setEditable(true);
edit.setText("Apply");
}
});
MenuItem save = toolbar.addItem("Save");
save.addClickListener(event -> saveConfiguration());
MenuItem load = toolbar.addItem("Load");
load.addClickListener(event -> loadConfiguration());
MenuItem addWidget = toolbar.addItem("Add widget");
for (WidgetConfig.WidgetType widgetType : WidgetConfig.WidgetType
.values()) {
addWidget.getSubMenu().addItem(widgetType.getLabel(),
event -> addWidget(widgetType));
}
MenuItem restore = toolbar.addItem("Restore default");
restore.addThemeNames("error");
restore.addClickListener(event -> restoreDefault());
add(toolbar);
}
private void saveConfiguration() {
// To save the dashboard configuration, we iterate over the current
// widgets in the dashboard and map them into configuration objects.
List<WidgetConfig> dashboardConfig = dashboard.getWidgets().stream()
.map(widget -> {
// Cast to our custom widget class and extract type,
// colspan, and rowspan
CustomWidget customWidget = (CustomWidget) widget;
return new WidgetConfig(customWidget.getType(),
widget.getColspan(), widget.getRowspan());
}).toList();
// Then save the configuration to the database or other storage
// In this example, we just store it in a session-scoped bean
dashboardStorage.save(dashboardConfig);
}
private void loadConfiguration() {
// Load the dashboard configuration from database or other storage
// In this example, we just load it from a session-scoped bean
// If no configuration is found, use the default configuration
List<WidgetConfig> dashboardConfig = dashboardStorage.load();
if (dashboardConfig == null) {
dashboardConfig = defaultConfig;
}
applyConfiguration(dashboardConfig);
}
private void applyConfiguration(List<WidgetConfig> dashboardConfig) {
// To apply a dashboard configuration, we first clear the dashboard and
// then create widgets based on the configuration
dashboard.removeAll();
for (WidgetConfig config : dashboardConfig) {
CustomWidget widget = createWidget(config);
dashboard.add(widget);
}
}
private CustomWidget createWidget(WidgetConfig config) {
// In this example all widget types have the same content, and the title
// is stored in the enum, so we can use generic logic to create a widget
CustomWidget widget = new CustomWidget(config.getType(),
config.getType().getLabel());
widget.setContent(createWidgetContent());
widget.setColspan(config.getColspan());
widget.setRowspan(config.getRowspan());
// In practice, different widget types will have different content. In
// that case you can use a switch statement to create the widget content
// based on the type.
//
// switch (config.type()) {
// case VISITORS:
// widget.setTitle("Visitors");
// widget.setContent(new VisitorsWidgetContent());
// break;
// ...
// }
return widget;
}
private void addWidget(WidgetConfig.WidgetType widgetType) {
// For adding a new widget, we retrieve the default configuration for
// the widget type and create a widget based on that configuration
WidgetConfig defaultWidgetConfig = defaultConfig.stream()
.filter(widgetConfig -> widgetConfig.getType() == widgetType)
.findFirst().orElseThrow();
CustomWidget widget = createWidget(defaultWidgetConfig);
dashboard.add(widget);
}
private void restoreDefault() {
// To restore defaults, we just apply the default configuration
applyConfiguration(defaultConfig);
}
// end::snippet[]
private Div createWidgetContent() {
Div content = new Div();
content.setClassName("dashboard-widget-content");
return content;
}
}
WidgetConfig.java
package com.vaadin.demo.component.dashboard;
import org.jspecify.annotations.NonNull;
// tag::snippet[]
// In order to save and load the dashboard configuration we need a class for storing
// the configuration of individual widgets. In this example we'll use a class that
// holds the widget type, colspan, and rowspan.
public class WidgetConfig {
public enum WidgetType {
VISITORS("Visitors"),
DOWNLOADS("Downloads"),
CONVERSIONS("Conversions"),
VISITORS_BY_COUNTRY("Visitors by country"),
BROWSER_DISTRIBUTION("Browser distribution"),
CAT_IMAGE("Cat image"),
VISITORS_BY_BROWSER("Visitors by browser");
private final String label;
WidgetType(String label) {
this.label = label;
}
public String getLabel() {
return label;
}
}
private WidgetType type;
private int colspan;
private int rowspan;
public WidgetConfig() {
}
public WidgetConfig(WidgetType type, int colspan, int rowspan) {
this.type = type;
this.colspan = colspan;
this.rowspan = rowspan;
}
@NonNull
public WidgetType getType() {
return type;
}
public void setType(WidgetType type) {
this.type = type;
}
@NonNull
public int getColspan() {
return colspan;
}
public void setColspan(int colspan) {
this.colspan = colspan;
}
@NonNull
public int getRowspan() {
return rowspan;
}
public void setRowspan(int rowspan) {
this.rowspan = rowspan;
}
}
// end::snippet[]
DashboardStorage.java
package com.vaadin.demo.component.dashboard;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.SessionScope;
import java.util.List;
// tag::snippet[]
@SessionScope
@Component
public class DashboardStorage {
private List<WidgetConfig> config;
public List<WidgetConfig> load() {
return config;
}
public void save(List<WidgetConfig> config) {
this.config = config;
}
}
// end::snippet[]
dashboard-editable.tsx
import React, { useCallback, useEffect } from 'react';
import { useSignal } from '@vaadin/hilla-react-signals';
import { MenuBar, type MenuBarItem } from '@vaadin/react-components';
import {
Dashboard,
type DashboardReactRendererProps,
DashboardWidget,
} from '@vaadin/react-components-pro';
import type WidgetConfig from 'Frontend/generated/com/vaadin/demo/component/dashboard/WidgetConfig';
import WidgetType from 'Frontend/generated/com/vaadin/demo/component/dashboard/WidgetConfig/WidgetType';
import { DashboardService } from 'Frontend/generated/endpoints';
// tag::snippet[]
// NOTE: This example uses the additional classes WidgetConfig and DashboardService,
// which you can find by switching to the respective file tab.
// This is the default configuration for the dashboard. Note that the order
// of the widgets in the array determines the order in which they are
// displayed in the dashboard.
const defaultConfig: WidgetConfig[] = [
{ type: WidgetType.VISITORS, colspan: 1, rowspan: 1 },
{ type: WidgetType.DOWNLOADS, colspan: 1, rowspan: 1 },
{ type: WidgetType.CONVERSIONS, colspan: 1, rowspan: 1 },
{ type: WidgetType.VISITORS_BY_COUNTRY, colspan: 1, rowspan: 2 },
{ type: WidgetType.BROWSER_DISTRIBUTION, colspan: 1, rowspan: 1 },
{ type: WidgetType.CAT_IMAGE, colspan: 1, rowspan: 1 },
{ type: WidgetType.VISITORS_BY_BROWSER, colspan: 2, rowspan: 1 },
];
// Define a mapping from widget types to human-readable titles
const widgetTitles: Record<WidgetType, string> = {
[WidgetType.VISITORS]: 'Visitors',
[WidgetType.DOWNLOADS]: 'Downloads',
[WidgetType.CONVERSIONS]: 'Conversions',
[WidgetType.VISITORS_BY_COUNTRY]: 'Visitors by country',
[WidgetType.BROWSER_DISTRIBUTION]: 'Browsers',
[WidgetType.CAT_IMAGE]: 'A kittykat!',
[WidgetType.VISITORS_BY_BROWSER]: 'Visitors by browser',
};
// Helper type to allow defining a custom action for a menu item
type CustomMenuItem = MenuBarItem & {
action?(): unknown;
};
function Example() {
const widgets = useSignal<WidgetConfig[]>([]);
const editable = useSignal<boolean>(false);
function toggleEditing() {
editable.value = !editable.value;
}
function save() {
// To save the dashboard configuration, we can just take the current
// widget items array and pass it to a server-side service for
// persisting it.
DashboardService.saveDashboard(widgets.value);
}
async function load() {
// To load the dashboard configuration, we just load it from a server-side
// service. If there is no configuration saved, we use a copy of the default
// configuration.
let config = await DashboardService.loadDashboard();
if (!config) {
config = [...defaultConfig];
}
widgets.value = config;
}
function addWidget(type: WidgetType) {
// For adding a new widget, we retrieve the default configuration for the
// widget type and add a copy of that to the widgets array.
const defaultWidgetConfig = defaultConfig.find((widget) => widget.type === type);
if (!defaultWidgetConfig) {
return;
}
widgets.value = [...widgets.value, { ...defaultWidgetConfig }];
}
function restore() {
// To restore defaults, we just set a copy of the default configuration
widgets.value = [...defaultConfig];
}
// Render function should be memoized to avoid unnecessary re-renders
const renderWidget = useCallback(({ item }: DashboardReactRendererProps<WidgetConfig>) => {
// This function is used to render the actual widgets into the dashboard.
// It is called by Dashboard once for each config in the widgets array
// and should return a React element. Note that the colspan and rowspan
// from the widget config are automatically applied by Dashboard.
// In this example all widget types have the same content, so we can use
// generic logic to render a widget.
const widget = (
<DashboardWidget widgetTitle={widgetTitles[item.type]}>
<div className="dashboard-widget-content" />
</DashboardWidget>
);
// In practice, different widget types will have different content.
// In that case you can use a switch statement to render the widget
// content based on the type.
//
// switch (item.type) {
// case WidgetType.Visitors:
// return (
// <DashboardWidget widgetTitle={widgetTitles[item.type]}>
// <VisitorsWidgetContent />
// </DashboardWidget>
// );
// ...
// }
return widget;
}, []);
// Load the initial configuration of the dashboard
useEffect(() => {
load();
}, []);
const menuItems: CustomMenuItem[] = [
{
text: editable.value ? 'Apply' : 'Edit',
action: toggleEditing,
theme: 'primary',
},
{
text: 'Save',
action: save,
},
{
text: 'Load',
action: load,
},
{
text: 'Add widget',
children: Object.values(WidgetType).map((type) => ({
text: widgetTitles[type as WidgetType],
action: () => addWidget(type as WidgetType),
})),
},
{
text: 'Restore default',
action: restore,
theme: 'error',
},
];
return (
<>
<MenuBar
theme="dropdown-indicators"
items={menuItems}
onItemSelected={(e) => (e.detail.value as CustomMenuItem).action?.()}
/>
<Dashboard
style={{
'--vaadin-dashboard-col-min-width': '150px',
'--vaadin-dashboard-col-max-count': '3',
}}
editable={editable.value}
items={widgets.value}
onDashboardItemMoved={(e) => {
// Store updated widgets after user has modified them
widgets.value = e.detail.items as WidgetConfig[];
}}
onDashboardItemResized={(e) => {
widgets.value = e.detail.items as WidgetConfig[];
}}
onDashboardItemRemoved={(e) => {
widgets.value = e.detail.items as WidgetConfig[];
}}
>
{renderWidget}
</Dashboard>
</>
);
}
// end::snippet[]
WidgetConfig.java
package com.vaadin.demo.component.dashboard;
import org.jspecify.annotations.NonNull;
// tag::snippet[]
// In order to save and load the dashboard configuration we need a class for storing
// the configuration of individual widgets. In this example we'll use a class that
// holds the widget type, colspan, and rowspan.
public class WidgetConfig {
public enum WidgetType {
VISITORS("Visitors"),
DOWNLOADS("Downloads"),
CONVERSIONS("Conversions"),
VISITORS_BY_COUNTRY("Visitors by country"),
BROWSER_DISTRIBUTION("Browser distribution"),
CAT_IMAGE("Cat image"),
VISITORS_BY_BROWSER("Visitors by browser");
private final String label;
WidgetType(String label) {
this.label = label;
}
public String getLabel() {
return label;
}
}
private WidgetType type;
private int colspan;
private int rowspan;
public WidgetConfig() {
}
public WidgetConfig(WidgetType type, int colspan, int rowspan) {
this.type = type;
this.colspan = colspan;
this.rowspan = rowspan;
}
@NonNull
public WidgetType getType() {
return type;
}
public void setType(WidgetType type) {
this.type = type;
}
@NonNull
public int getColspan() {
return colspan;
}
public void setColspan(int colspan) {
this.colspan = colspan;
}
@NonNull
public int getRowspan() {
return rowspan;
}
public void setRowspan(int rowspan) {
this.rowspan = rowspan;
}
}
// end::snippet[]
DashboardService.java
package com.vaadin.demo.component.dashboard;
import com.vaadin.hilla.BrowserCallable;
import org.jspecify.annotations.NonNull;
import java.util.List;
// tag::snippet[]
// This is a simple browser-callable server that allows saving and loading a
// dashboard configuration. For this example, we just store the configuration
// in a session-scoped bean. In practice, you'd want to store the configuration
// in a database or some other persistent storage along with the user ID.
@BrowserCallable
public class DashboardService {
private final DashboardStorage dashboardStorage;
public DashboardService(DashboardStorage dashboardStorage) {
this.dashboardStorage = dashboardStorage;
}
public void saveDashboard(@NonNull List<@NonNull WidgetConfig> config) {
dashboardStorage.save(config);
}
public List<@NonNull WidgetConfig> loadDashboard() {
return dashboardStorage.load();
}
}
// end::snippet[]
dashboard-editable.ts
import '@vaadin/menu-bar';
import '@vaadin/dashboard/vaadin-dashboard.js';
import '@vaadin/dashboard/vaadin-dashboard-widget.js';
import { html, LitElement, render } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import type {
Dashboard,
DashboardItemMovedEvent,
DashboardItemRemovedEvent,
DashboardItemResizedEvent,
} from '@vaadin/dashboard';
import type { MenuBarItem, MenuBarItemSelectedEvent } from '@vaadin/menu-bar';
import type WidgetConfig from 'Frontend/generated/com/vaadin/demo/component/dashboard/WidgetConfig';
import WidgetType from 'Frontend/generated/com/vaadin/demo/component/dashboard/WidgetConfig/WidgetType';
import { DashboardService } from 'Frontend/generated/endpoints';
import { applyTheme } from 'Frontend/generated/theme';
// tag::snippet[]
// NOTE: This example uses the additional classes WidgetConfig and DashboardService,
// which you can find by switching to the respective file tab.
// This is the default configuration for the dashboard. Note that the order
// of the widgets in the array determines the order in which they are
// displayed in the dashboard.
const defaultConfig: WidgetConfig[] = [
{ type: WidgetType.VISITORS, colspan: 1, rowspan: 1 },
{ type: WidgetType.DOWNLOADS, colspan: 1, rowspan: 1 },
{ type: WidgetType.CONVERSIONS, colspan: 1, rowspan: 1 },
{ type: WidgetType.VISITORS_BY_COUNTRY, colspan: 1, rowspan: 2 },
{ type: WidgetType.BROWSER_DISTRIBUTION, colspan: 1, rowspan: 1 },
{ type: WidgetType.CAT_IMAGE, colspan: 1, rowspan: 1 },
{ type: WidgetType.VISITORS_BY_BROWSER, colspan: 2, rowspan: 1 },
];
// Define a mapping from widget types to human-readable titles
const widgetTitles: Record<WidgetType, string> = {
[WidgetType.VISITORS]: 'Visitors',
[WidgetType.DOWNLOADS]: 'Downloads',
[WidgetType.CONVERSIONS]: 'Conversions',
[WidgetType.VISITORS_BY_COUNTRY]: 'Visitors by country',
[WidgetType.BROWSER_DISTRIBUTION]: 'Browsers',
[WidgetType.CAT_IMAGE]: 'A kittykat!',
[WidgetType.VISITORS_BY_BROWSER]: 'Visitors by browser',
};
// Helper type to allow defining a custom action for a menu item
type CustomMenuItem = MenuBarItem & {
action?(): unknown;
};
@customElement('dashboard-editable')
export class Example extends LitElement {
@state()
widgets: WidgetConfig[] = [];
@state()
editable = false;
protected override createRenderRoot() {
const root = super.createRenderRoot();
// Apply custom theme (only supported if your app uses one)
applyTheme(root);
return root;
}
firstUpdated() {
// Load the initial configuration of the dashboard
this.load();
}
toggleEditing() {
this.editable = !this.editable;
}
async load() {
// To load the dashboard configuration, we just load it from a server-side
// service. If there is no configuration saved, we use a copy of the default
// configuration.
const config = await DashboardService.loadDashboard();
this.widgets = config ?? [...defaultConfig];
}
save() {
// To save the dashboard configuration, we can just take the current
// widget items array and pass it to a server-side service for
// persisting it.
DashboardService.saveDashboard(this.widgets);
}
addWidget(type: WidgetType) {
// For adding a new widget, we retrieve the default configuration for the
// widget type and add a copy of that to the widgets array.
const defaultWidgetConfig = defaultConfig.find((widget) => widget.type === type);
if (defaultWidgetConfig) {
this.widgets = [...this.widgets, { ...defaultWidgetConfig }];
}
}
restore() {
// To restore defaults, we just set a copy of the default configuration
this.widgets = [...defaultConfig];
}
render() {
return html` ${this.renderMenu()} ${this.renderDashboard()} `;
}
renderMenu() {
const menuItems = [
{
text: this.editable ? 'Apply' : 'Edit',
action: this.toggleEditing.bind(this),
theme: 'primary',
},
{
text: 'Save',
action: this.save.bind(this),
},
{
text: 'Load',
action: this.load.bind(this),
},
{
text: 'Add widget',
children: Object.values(WidgetType).map((type) => ({
text: widgetTitles[type],
action: () => this.addWidget(type),
})),
},
{
text: 'Restore default',
action: this.restore.bind(this),
theme: 'error',
},
];
return html`
<vaadin-menu-bar
.items="${menuItems}"
@item-selected="${(e: MenuBarItemSelectedEvent) => {
const item = e.detail.value as CustomMenuItem;
item.action?.();
}}"
theme="dropdown-indicators"
></vaadin-menu-bar>
`;
}
renderDashboard() {
return html`
<vaadin-dashboard
style="--vaadin-dashboard-col-min-width: 150px; --vaadin-dashboard-col-max-count: 3;"
.editable="${this.editable}"
.items="${this.widgets}"
.renderer="${this.renderWidget}"
@dashboard-item-moved="${(e: DashboardItemMovedEvent<WidgetConfig>) => {
// Store updated widgets after user has modified them
this.widgets = e.detail.items as WidgetConfig[];
}}"
@dashboard-item-resized="${(e: DashboardItemResizedEvent<WidgetConfig>) => {
this.widgets = e.detail.items as WidgetConfig[];
}}"
@dashboard-item-removed="${(e: DashboardItemRemovedEvent<WidgetConfig>) => {
this.widgets = e.detail.items as WidgetConfig[];
}}"
></vaadin-dashboard>
`;
}
renderWidget(root: HTMLElement, _dashboard: Dashboard, { item }: { item: WidgetConfig }) {
// This method is used to render the actual widgets into the dashboard.
// It is called by vaadin-dashboard once for each config in the widgets
// array and should render content into the provided root element. Note
// that the colspan and rowspan from the widget config are
// automatically applied by vaadin-dashboard.
// In this example all widget types have the same content, so we can
// use generic logic to render a widget.
render(
html`
<vaadin-dashboard-widget .widgetTitle="${widgetTitles[item.type]}">
<div class="dashboard-widget-content"></div>
</vaadin-dashboard-widget>
`,
root
);
// In practice, different widget types will have different content.
// In that case you can use a switch statement to render the widget
// content based on the type.
//
// let widget: TemplateResult;
//
// switch (item.type) {
// case WidgetType.Visitors:
// widget = html`
// <vaadin-dashboard-widget .widgetTitle="Visitors">
// <visitors-widget-content></visitors-widget-content>
// </vaadin-dashboard-widget>
// `;
// break;
// ...
// }
//
// render(widget, root);
}
}
// end::snippet[]
WidgetConfig.java
package com.vaadin.demo.component.dashboard;
import org.jspecify.annotations.NonNull;
// tag::snippet[]
// In order to save and load the dashboard configuration we need a class for storing
// the configuration of individual widgets. In this example we'll use a class that
// holds the widget type, colspan, and rowspan.
public class WidgetConfig {
public enum WidgetType {
VISITORS("Visitors"),
DOWNLOADS("Downloads"),
CONVERSIONS("Conversions"),
VISITORS_BY_COUNTRY("Visitors by country"),
BROWSER_DISTRIBUTION("Browser distribution"),
CAT_IMAGE("Cat image"),
VISITORS_BY_BROWSER("Visitors by browser");
private final String label;
WidgetType(String label) {
this.label = label;
}
public String getLabel() {
return label;
}
}
private WidgetType type;
private int colspan;
private int rowspan;
public WidgetConfig() {
}
public WidgetConfig(WidgetType type, int colspan, int rowspan) {
this.type = type;
this.colspan = colspan;
this.rowspan = rowspan;
}
@NonNull
public WidgetType getType() {
return type;
}
public void setType(WidgetType type) {
this.type = type;
}
@NonNull
public int getColspan() {
return colspan;
}
public void setColspan(int colspan) {
this.colspan = colspan;
}
@NonNull
public int getRowspan() {
return rowspan;
}
public void setRowspan(int rowspan) {
this.rowspan = rowspan;
}
}
// end::snippet[]
DashboardService.java
package com.vaadin.demo.component.dashboard;
import com.vaadin.hilla.BrowserCallable;
import org.jspecify.annotations.NonNull;
import java.util.List;
// tag::snippet[]
// This is a simple browser-callable server that allows saving and loading a
// dashboard configuration. For this example, we just store the configuration
// in a session-scoped bean. In practice, you'd want to store the configuration
// in a database or some other persistent storage along with the user ID.
@BrowserCallable
public class DashboardService {
private final DashboardStorage dashboardStorage;
public DashboardService(DashboardStorage dashboardStorage) {
this.dashboardStorage = dashboardStorage;
}
public void saveDashboard(@NonNull List<@NonNull WidgetConfig> config) {
dashboardStorage.save(config);
}
public List<@NonNull WidgetConfig> loadDashboard() {
return dashboardStorage.load();
}
}
// end::snippet[]