diff --git a/.github/styles/config/vocabularies/Docs/accept.txt b/.github/styles/config/vocabularies/Docs/accept.txt index 8656a7fc40..d1f2e143c1 100644 --- a/.github/styles/config/vocabularies/Docs/accept.txt +++ b/.github/styles/config/vocabularies/Docs/accept.txt @@ -204,6 +204,8 @@ WebDriver webpack websockets? WebSphere +[wW]idget +widget's WildFly Workbox Xcode diff --git a/articles/components/dashboard/dashboard.png b/articles/components/dashboard/dashboard.png new file mode 100644 index 0000000000..7537c53c2b Binary files /dev/null and b/articles/components/dashboard/dashboard.png differ diff --git a/articles/components/dashboard/index.adoc b/articles/components/dashboard/index.adoc new file mode 100644 index 0000000000..e76f0aae42 --- /dev/null +++ b/articles/components/dashboard/index.adoc @@ -0,0 +1,566 @@ +--- +tab-title: Usage +layout: tabbed-page +title: Dashboard +description: A component for building static dashboard layouts and dynamic, user-configurable dashboards. +page-links: + - 'API: https://cdn.vaadin.com/vaadin-web-components/{moduleNpmVersion:@vaadin/dashboard}/#/elements/vaadin-dashboard[TypeScript] / https://vaadin.com/api/platform/{moduleMavenVersion:com.vaadin:vaadin}/com/vaadin/flow/component/dashboard/Dashboard.html[Java]' + - 'Source: https://github.com/vaadin/web-components/tree/v{moduleNpmVersion:@vaadin/dashboard}/packages/dashboard[TypeScript] / https://github.com/vaadin/flow-components/tree/{moduleMavenVersion:com.vaadin:vaadin}/vaadin-dashboard-flow-parent[Java]' +section-nav: commercial badge-preview +version: since:com.vaadin:vaadin@V24.6 +--- + + += Dashboard +:experimental: + +// tag::description[] +A component for building static dashboard layouts and dynamic, user-configurable dashboards. +// end::description[] + +:commercial-feature: Dashboard +include::{articles}/_commercial-banner.adoc[opts=optional] + +:preview-feature: Dashboard +:feature-flag: com.vaadin.experimental.dashboardComponent +include::{articles}/_preview-banner.adoc[opts=optional] + +[.example] +-- +ifdef::flow[] +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/dashboard/DashboardBasic.java[render,tags=snippet,indent=0,group=Flow] +---- +endif::[] + +ifdef::react[] +[source,tsx] +---- +include::{root}/frontend/demo/component/dashboard/react/dashboard-basic.tsx[render,tags=snippet,indent=0,group=React] +---- +endif::[] + +ifdef::lit[] +[source,html] +---- +include::{root}/frontend/demo/component/dashboard/dashboard-basic.ts[render,tags=snippet,indent=0,group=Lit] +---- +endif::[] +-- + +Widgets are placed in columns and rows automatically, in the order supplied, based on the Dashboard's width and the column configuration. As the Dashboard's width changes, the number of columns is automatically adjusted based on their configured minimum and maximum width, and the widget positions are adjusted accordingly. Widgets can not be explicitly placed in a particular column or row. + +The Dashboard provides built-in vertical scrolling, provided that its height has been specified. + +The Dashboard component can be used in two ways: + +* <<#static-dashboards,Static>>: declaratively / imperatively defining the widgets. The React and Web Components for this are `` and `` respectively. +* <<#dynamic-dashboards,Dynamic>>: widgets generated from data using a renderer; supports edit mode that allows the end user to move, resize and remove widgets. The React and Web Components for this are `` and ``, respectively. + +In Flow, the same `Dashboard` class is used for both approaches. + + +== Configuration + +The following configuration options are available for the dashboard: + +=== Columns and Rows + +Column width can vary between a minimum and maximum size. The default maximum width is `1fr`, which allows the columns to expand to fill any available space. If a fixed length value is provided, empty space is left at the end of rows once the columns reach their maximum width. + +By default there is no limit on the number of columns, but one can be provided if needed. + +The height of each dashboard row is determined by the tallest widget currently in that row (whose height in turn is determined by its contents). A minimum row height determines the height of empty rows, such as when a widget's row span is stretched into a previously unoccupied row. The minimum height can be configured. + +[.example] +-- +ifdef::flow[] +[source,java] +---- + +dashboard.setMinimumColumnWidth("150px"); +dashboard.setMaximumColumnWidth("300px"); +dashboard.setMaximumColumnCount(4); +dashboard.setMinimumRowHeight("100px"); +---- +endif::[] + +ifdef::react[] +[source,tsx] +---- + + +... + +---- +endif::[] + +ifdef::lit[] +[source,html] +---- + + +... + +---- +endif::[] +-- + +=== Whitespace + +The horizontal and vertical spacing between widgets, and the padding along the dashboard's edges, can be configured. + +[.example] +-- +ifdef::flow[] +[source,java] +---- + +dashboard.setGap("10px"); +dashboard.setPadding("20px"); +---- +endif::[] + +ifdef::react[] +[source,tsx] +---- + + +... + +---- +endif::[] + +ifdef::lit[] +[source,html] +---- + + +... + +---- +endif::[] +-- + +=== Dense Layout +This mode uses the dense packing algorithm in the CSS grid layout model, which attempts to fill in empty slots in the layout by placing smaller widgets in them, potentially affecting the order of the widgets. It should be used with caution in user-configurable dashboards, as the automatic reordering of widgets may be confusing during editing. + +[.example] +-- +ifdef::flow[] +[source,java] +---- + +dashboard.setDenseLayout(true); +---- +endif::[] + +ifdef::react[] +[source,tsx] +---- + + +... + +---- +endif::[] + +ifdef::lit[] +[source,html] +---- + + +... + +---- +endif::[] +-- + +[.example.render-only] +-- +[source,typescript] +---- +include::{root}/frontend/demo/component/dashboard/dashboard-dense-layout.ts[render,indent=0] +---- +-- + +== Widgets + +Widgets consist of a content area and a header containing the widget's title and a slot for additional elements. + +[.example] +-- +ifdef::flow[] +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/dashboard/DashboardWidgetContents.java[render,tags=snippet,indent=0,group=Flow] +---- +endif::[] + +ifdef::react[] +[source,tsx] +---- +include::{root}/frontend/demo/component/dashboard/react/dashboard-widget-contents.tsx[render,tags=snippet,indent=0,group=React] +---- +endif::[] + +ifdef::lit[] +[source,html] +---- +include::{root}/frontend/demo/component/dashboard/dashboard-widget-contents.ts[render,tags=snippet,indent=0,group=Lit] +---- +endif::[] +-- + +The column span and row span can be set to make the widget take up more than one column or row in the dashboard's layout. The actual number of columns a widget spans is limited by the number of columns currently in the dashboard, however. + + +=== Widget Content Sizing + +The height of a widget's contents determine its default height, which can be expanded by row span and/or other widgets on the same dashboard row. The widget does not scroll, so the contents may need to incorporate a scrollable area of its own (e.g. by way of a <<../scroller#,Scroller>>) to accommodate a height smaller than its contents. + +The widget's width, on the other hand, is determined by the current column width and the widget's column span. + +Contents that should cover the entire widget area should therefore be configured with 100% width and height, as well as a minimum height corresponding to its desired default height. + + +[#static-dashboards] +== Static Dashboards + +Static dashboards are populated declaratively (in React and Lit) / imperatively (in Flow), like normal layouts. They're a good choice for hard-coded dashboards. + +[cols="1,1m"] +|=== + +|Flow +|Dashboard + +|React +| + +|Lit Web Component +| + +|=== + +[.example] +-- +ifdef::flow[] +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/dashboard/DashboardBasic.java[render,tags=snippet,indent=0,group=Flow] +---- +endif::[] + +ifdef::react[] +[source,tsx] +---- +include::{root}/frontend/demo/component/dashboard/react/dashboard-basic.tsx[render,tags=snippet,indent=0,group=React] +---- +endif::[] + +ifdef::lit[] +[source,html] +---- +include::{root}/frontend/demo/component/dashboard/dashboard-basic.ts[render,tags=snippet,indent=0,group=Lit] +---- +endif::[] +-- + + +[#dynamic-dashboards] +== Dynamic, Editable Dashboards + +Dynamic dashboards provide end user editability out of the box. They're populated through a data binding API coupled with a widget renderer function, so that their contents can be easily persisted to and loaded from storage, such as a database. + +[cols="1,1m"] +|=== + +|Flow +|Dashboard + +|React +| + +|Lit Web Component +| + +|=== + +[.example] +-- +ifdef::flow[] +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/dashboard/DashboardEditable.java[render,tags=snippet,indent=0,group=Flow] +---- +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/dashboard/WidgetConfig.java[tags=snippet,indent=0,group=Flow] +---- +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/dashboard/DashboardStorage.java[tags=snippet,indent=0,group=Flow] +---- +endif::[] + +ifdef::react[] +[source,tsx] +---- +include::{root}/frontend/demo/component/dashboard/react/dashboard-editable.tsx[render,tags=snippet,indent=0,group=React] +---- +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/dashboard/WidgetConfig.java[tags=snippet,indent=0,group=React] +---- +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/dashboard/DashboardService.java[tags=snippet,indent=0,group=React] +---- +endif::[] + +ifdef::lit[] +[source,typescript] +---- +include::{root}/frontend/demo/component/dashboard/dashboard-editable.ts[render,tags=snippet,indent=0,group=Lit] +---- +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/dashboard/WidgetConfig.java[tags=snippet,indent=0,group=Lit] +---- +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/dashboard/DashboardService.java[tags=snippet,indent=0,group=Lit] +---- +endif::[] +-- + + +=== Editing + +Dynamic dashboards can be made user editable by toggling on editing mode, as seen in the sample above. + +.Editing mode should be temporary +[NOTE] +==== +Editing mode is intended as a temporary state that is engaged when the end user wants to edit the dashboard's contents, and disengaged when they're done editing, at which point the widget configuration is typically persisted to storage. +==== + +The following operations are available in editing mode: + +==== Widget Selection by Keyboard + +In editing mode, widgets can be selected by keyboard by moving focus to the desired widget using the kbd:[Tab] key and pressing kbd:[Space] or kbd:[Enter]. Once selected, arrow keys can be used to move and resize widgets, and to engage the accessible move and resize modes. + +Widget selection is not required for editing by pointer device. + +==== Moving Widgets + +In editing mode, widgets can be moved around by + +* Drag & drop +* Arrow keys, once the widget has been selected +* An accessible move-mode engaged by clicking the drag-handle in the widget's top left corner. Move-mode is disengaged by clicking the apply-button in the widget's center, or by pressing kbd:[Esc]. + +(Currently widgets can only be moved backwards and forwards; moving a widget past the start or end of a row moves it to the preceding or following row.) + +==== Resizing Widgets + +In editing mode, widgets can be resized by increasing and decreasing their column span and row span by + +* Dragging from the drag-handle in the widget's bottom right corner +* kbd:[Shift] + arrow keys, once the widget has been selected +* An accessible resize-mode engaged by clicking the resize-handle. Resize-mode is disengaged by clicking the apply-button in the widget's center, or by pressing kbd:[Esc]. + +==== Removing Widgets + +In editing mode, widgets can be removed by clicking the remove-button in the widget's top right corner. + +==== Adding Widgets + +Dashboard has no built-in mechanism for adding new widgets, but this can easily be implemented using an external widget selector, such as a <<../select#,Select>> drop-down, that adds the corresponding item to the dashboard. + +==== Screen Reader Announcements + +Although widget selection is announced via the widget's title, and the various buttons all have accessible names, the component does not provide announcements of changes to the widget's position and size out of the box. These can be provided by listening to related events emitted by the component and updating custom live regions with appropriate announcements. + +[.example] +-- +ifdef::flow[] +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/dashboard/DashboardAnnouncements.java[render,tags=snippet,indent=0,group=Flow] +---- +endif::[] + +ifdef::react[] +[source,tsx] +---- +include::{root}/frontend/demo/component/dashboard/react/dashboard-announcements.tsx[render,tags=snippet,indent=0,group=React] +---- +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/dashboard/WidgetConfig.java[tags=snippet,indent=0,group=React] +---- +endif::[] + +ifdef::lit[] +[source,typescript] +---- +include::{root}/frontend/demo/component/dashboard/dashboard-announcements.ts[render,tags=snippet,indent=0,group=Lit] +---- +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/dashboard/WidgetConfig.java[tags=snippet,indent=0,group=Lit] +---- +endif::[] +-- + + +=== Persisting and Loading Widgets + +Dynamic dashboards, with their user-editable capabilities, often require the ability to persist and load customized widget configurations to and from storage, such as a database. + +The most straightforward way to persist widget configurations is by defining a custom widget/item type. This type can include additional custom metadata relevant to the widget content, in addition to the built-in widget/item properties. + +Once you've defined your custom type, you can establish a mapping between your data model and the widget configuration. This involves: + +- Loading: When loading the persisted configuration, map the data from your storage to individual widget/item instances of your custom type. Each record corresponds to a single widget on the dashboard. +- Saving: When saving the user's customized dashboard layout, map the current configuration (e.g., column span, row span, type, custom metadata) of your dashboard's widgets back to your data model format. + +This approach allows for flexible persistence of dashboard configurations, enabling users to save and load their customized layouts across sessions. + +For a simple example of how to implement this persistence approach, refer to the <<#dynamic-dashboards>> section above. While the example doesn’t explicitly demonstrate data persistence, it illustrates the concept of defining a custom type for the dashboard widgets. The specific implementation details depend on your chosen storage mechanism and data model. + + +== Dashboard Sections + +Complex dashboards can benefit from being divided into titled sections. Dashboard Sections always span the full width of the dashboard, and follow the same column and row configuration as the dashboard itself. They support the same moving and removal operations in editing mode as widgets. + +[.example] +-- +ifdef::flow[] +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/dashboard/DashboardSections.java[render,tags=snippet,indent=0,group=Flow] +---- +endif::[] + +ifdef::react[] +[source,tsx] +---- +include::{root}/frontend/demo/component/dashboard/react/dashboard-sections.tsx[render,tags=snippet,indent=0,group=React] +---- +endif::[] + +ifdef::lit[] +[source,html] +---- +include::{root}/frontend/demo/component/dashboard/dashboard-sections.ts[render,tags=snippet,indent=0,group=Lit] +---- +endif::[] +-- + +== Internationalization + +The following texts in the dashboard can be localized through the internationalization object: + + +[cols="1m,2"] +|=== +| Property | Description + +|selectWidget +|Widget selection trigger + +|deselectWidget +|Widget deselection trigger + +|selectSection +|Section selection trigger + +|deselectSection +|Section deselection trigger + +|move +|Button that engages move-mode + +|moveForward +|Move forward button (in move-mode) + +|moveBackward +|Move backward button (in move-mode) + +|moveApply +|Button that disengages move-mode + +|resize +|Button that engages resize-mode + +|resizeGrowWidth +|Grow width button (in resize-mode) + +|resizeShrinkWidth +|Shrink width button (in resize-mode) + +|resizeGrowHeight +|Grow height button (in resize-mode) + +|resizeShrinkHeight +|Shrink height button (in resize-mode) + +|resizeApply +|Button that disengages resize-mode + +|remove +|Remove button + +|=== + + +[.example] +-- +ifdef::flow[] +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/dashboard/DashboardInternationalisation.java[tags=snippet,indent=0,group=Flow] +---- +endif::[] + +ifdef::react[] +[source,tsx] +---- +include::{root}/frontend/demo/component/dashboard/react/dashboard-internationalisation.tsx[tags=snippet,indent=0,group=React] +---- +endif::[] + +ifdef::lit[] +[source,typescript] +---- +include::{root}/frontend/demo/component/dashboard/dashboard-internationalisation.ts[tags=snippet,indent=0,group=Lit] +---- +endif::[] +-- + +//// +UNCOMMENT ONCE CARD PAGE EXISTS + +== Related Components + +|=== +|Component | Usage Recommendations +|<<../card#, Card>> +|Generic card component that can be used in any layout +|=== +//// + +[discussion-id]`d59db2ee-c3dd-446d-bd0d-40224b1f141e` diff --git a/articles/components/dashboard/styling.adoc b/articles/components/dashboard/styling.adoc new file mode 100644 index 0000000000..9141899950 --- /dev/null +++ b/articles/components/dashboard/styling.adoc @@ -0,0 +1,136 @@ +--- +title: Styling +description: Styling API reference for the Dashboard component. +order: 50 +--- += Styling + + +include::../_styling-section-theming-props.adoc[tag=style-properties] + +[cols="1,2,1"] +|=== +| Feature | Property | Default Value + +|Minimum column width +|`--vaadin-dashboard-col-min-width` +|`25rem` + +|Maximum column width +|`--vaadin-dashboard-col-min-width` +|`1fr` + +|Maximum column count +|`--vaadin-dashboard-col-max-count` +| + +|Minimum row height +|`--vaadin-dashboard-row-min-height` +|`12rem` + +|Dashboard padding (around contents) +|`--vaadin-dashboard-padding` +|`--lumo-space-m` + +|Widget gap +|`--vaadin-dashboard-gap` +|`--lumo-space-m` + +|Widget background +|`--vaadin-dashboard-widget-background` +|`--lumo-base-color` + +|Widget corner radius +|`--vaadin-dashboard-widget-border-radius` +|`--lumo-border-radius-l` + +|Widget border width +|`--vaadin-dashboard-widget-border-width` +|`--1px` + +|Widget border color +|`--vaadin-dashboard-widget-border-color` +|`--lumo-contrast-20pct` + +|Widget shadow (non-editable) +|`--vaadin-dashboard-widget-shadow` +|`0 0 0 0 transparent` + +|Editable widget shadow +|`--vaadin-dashboard-widget-editable-shadow` +|`--lumo-box-shadow-s` + +|Selected widget shadow +|`--vaadin-dashboard-widget-selected-shadow` +|`0 2px 4px -1px var(--lumo-primary-color-10pct), 0 3px 12px -1px var(--lumo-primary-color-50pct);` + +|Drop target / resize box background color +|`--vaadin-dashboard-drop-target-background-color` +|`--lumo-primary-color-10pct` + +|Drop target / resize box border +|`--vaadin-dashboard-drop-target-border` +|`--1px dashed var(--lumo-primary-color-50pct)` + +|=== + + + +include::../_styling-section-intros.adoc[tag=selectors] + +Dashboard root element:: `vaadin-dashboard` +Static Dashboard Layout (React & Lit):: `vaadin-dashboard-layout` + + +=== Widgets + +Root element:: `vaadin-dashboard-widget` + +==== Widget States +Editable:: `vaadin-dashboard-widget++++++**[editable]**` +Focused:: `vaadin-dashboard-widget++++++**[focused]**` +Selected:: `vaadin-dashboard-widget++++++**[selected]**` +Being dragged:: `vaadin-dashboard-widget++++++**[dragging]**` +Being resized:: `vaadin-dashboard-widget++++++**[resizing]**` +Accessible move mode:: `vaadin-dashboard-widget++++++**[move-mode]**` +Accessible resize mode:: `vaadin-dashboard-widget++++++**[resize-mode]**` + +==== Widget Parts +Header:: `vaadin-dashboard-widget++++++**::part(header)**` +Title:: `vaadin-dashboard-widget++++++**::part(title)**` +Content area:: `vaadin-dashboard-widget++++++**::part(content)**` +Drag handle / Move button:: `vaadin-dashboard-widget++++++**::part(move-button)**` +Remove button:: `vaadin-dashboard-widget++++++**::part(remove-button)**` +Resize handle / button:: `vaadin-dashboard-widget++++++**::part(resize-button)**` + + +=== Sections + +Root element:: `vaadin-dashboard-section` + +==== Section States +Editable:: `vaadin-dashboard-section++++++**[editable]**` +Focused:: `vaadin-dashboard-section++++++**[focused]**` +Selected:: `vaadin-dashboard-section++++++**[selected]**` +Being dragged:: `vaadin-dashboard-section++++++**[dragging]**` +Accessible move mode:: `vaadin-dashboard-section++++++**[move-mode]**` + +==== Section Parts +Header:: `vaadin-dashboard-section++++++**::part(header)**` +Title:: `vaadin-dashboard-section++++++**::part(title)**` +Drag handle / Move button:: `vaadin-dashboard-section++++++**::part(move-button)**` +Remove button:: `vaadin-dashboard-section++++++**::part(remove-button)**` + + +=== Accessible Move & Resize Modes + +To target move-mode buttons on Sections, replace `vaadin-dashboard-widget` with `vaadin-dashboard-section` in the selectors below. + +Move backward button:: `vaadin-dashboard-widget++++++**::part(move-backward-button)**` +Move forward button:: `vaadin-dashboard-widget++++++**::part(move-forward-button)**` +Move apply button:: `vaadin-dashboard-widget++++++**::part(move-apply-button)**` +Shrink width button:: `vaadin-dashboard-widget++++++**::part(resize-shrink-width-button)**` +Grow width button:: `vaadin-dashboard-widget++++++**::part(resize-grow-width-button)**` +Shrink height button:: `vaadin-dashboard-widget++++++**::part(resize-shrink-width-button)**` +Grow height button:: `vaadin-dashboard-widget++++++**::part(resize-grow-width-button)**` +Resize apply button:: `vaadin-dashboard-widget++++++**::part(resize-apply-button)**` diff --git a/articles/components/index.adoc b/articles/components/index.adoc index 80dcb4827a..f23b14d1f2 100644 --- a/articles/components/index.adoc +++ b/articles/components/index.adoc @@ -633,6 +633,17 @@ include::board/index.adoc[tag=description] <<{components-path-prefix}board#,See Board>> +[.component-card.commercial] +=== Dashboard + +image::{components-path-prefix}dashboard/dashboard.png["", opts=inline, role="banner"] + +include::dashboard/index.adoc[tag=description] + +[.sr-only] +<<{components-path-prefix}dashboard#,See Dashboard>> + + [.component-card] === Form Layout diff --git a/frontend/demo/component/dashboard/dashboard-announcements.ts b/frontend/demo/component/dashboard/dashboard-announcements.ts new file mode 100644 index 0000000000..897bfc3916 --- /dev/null +++ b/frontend/demo/component/dashboard/dashboard-announcements.ts @@ -0,0 +1,149 @@ +import 'Frontend/demo/init'; // hidden-source-line +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, + DashboardItemMoveModeChangedEvent, + DashboardItemRemovedEvent, + DashboardItemResizedEvent, + DashboardItemResizeModeChangedEvent, + DashboardItemSelectedChangedEvent, +} from '@vaadin/dashboard'; +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 { applyTheme } from 'Frontend/generated/theme'; + +// Define a mapping from widget types to human-readable titles +const widgetTitles: Record = { + [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', +}; + +@customElement('dashboard-announcements') +export class Example extends LitElement { + @state() + widgets: 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 }, + ]; + + // tag::snippet[] + @state() + announcement: string = ''; + + // end::snippet[] + + protected override createRenderRoot() { + const root = super.createRenderRoot(); + // Apply custom theme (only supported if your app uses one) + applyTheme(root); + return root; + } + + // tag::snippet[] + handleSelectedChange(e: DashboardItemSelectedChangedEvent) { + // This event is fired when the user starts or stops editing a widget + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const title = widgetTitles[(e.detail.item as WidgetConfig).type]; + const selected = e.detail.value ? 'selected' : 'unselected'; + + this.announcement = `Widget ${title} ${selected}`; + } + + handleMoveModeChange(e: DashboardItemMoveModeChangedEvent) { + // This event is fired when the user enters or exits move mode + if (e.detail.value) { + this.announcement = 'Entered move mode'; + } else { + this.announcement = 'Exited move mode'; + } + } + + handleResizeModeChange(e: DashboardItemResizeModeChangedEvent) { + // This event is fired when the user enters or exits resize mode + if (e.detail.value) { + this.announcement = 'Entered resize mode'; + } else { + this.announcement = 'Exited resize mode'; + } + } + + handleMove(e: DashboardItemMovedEvent) { + // This event is fired when the user moves a widget + const index = e.detail.items.findIndex((widget) => widget === e.detail.item) + 1; + const total = e.detail.items.length; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const title = widgetTitles[(e.detail.item as WidgetConfig).type]; + + this.announcement = `Moved widget ${title} to position ${index} of ${total}`; + } + + handleResize(e: DashboardItemResizedEvent) { + // This event is fired when the user resizes a widget + const colspan = e.detail.item.colspan; + const rowspan = e.detail.item.rowspan; + const title = widgetTitles[e.detail.item.type]; + + this.announcement = `Resized widget ${title} to ${colspan} columns, ${rowspan} rows`; + } + + handleRemove(e: DashboardItemRemovedEvent) { + // This event is fired when the user removes a widget + const title = widgetTitles[(e.detail.item as WidgetConfig).type]; + + this.announcement = `Removed widget ${title}`; + } + + render() { + return html` +

Live announcement:

+ +
${this.announcement}
+ + `; + } + + // end::snippet[] + + renderWidget(root: HTMLElement, _dashboard: Dashboard, { item }: { item: WidgetConfig }) { + render( + html` + +
+
+ `, + root + ); + } +} diff --git a/frontend/demo/component/dashboard/dashboard-basic.ts b/frontend/demo/component/dashboard/dashboard-basic.ts new file mode 100644 index 0000000000..1bcc6d04ac --- /dev/null +++ b/frontend/demo/component/dashboard/dashboard-basic.ts @@ -0,0 +1,54 @@ +import 'Frontend/demo/init'; // hidden-source-line +import '@vaadin/dashboard/vaadin-dashboard-layout.js'; +import '@vaadin/dashboard/vaadin-dashboard-widget.js'; +import { html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { applyTheme } from 'Frontend/generated/theme'; + +@customElement('dashboard-basic') +export class Example extends LitElement { + protected override createRenderRoot() { + const root = super.createRenderRoot(); + // Apply custom theme (only supported if your app uses one) + applyTheme(root); + return root; + } + + render() { + return html` + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + `; + } +} diff --git a/frontend/demo/component/dashboard/dashboard-dense-layout.ts b/frontend/demo/component/dashboard/dashboard-dense-layout.ts new file mode 100644 index 0000000000..6bb0bad0ae --- /dev/null +++ b/frontend/demo/component/dashboard/dashboard-dense-layout.ts @@ -0,0 +1,54 @@ +import 'Frontend/demo/init'; // hidden-source-line +import '@vaadin/checkbox'; +import '@vaadin/dashboard/vaadin-dashboard-layout.js'; +import '@vaadin/dashboard/vaadin-dashboard-widget.js'; +import { html, LitElement } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { applyTheme } from 'Frontend/generated/theme'; + +@customElement('dashboard-dense-layout') +export class Example extends LitElement { + protected override createRenderRoot() { + const root = super.createRenderRoot(); + // Apply custom theme (only supported if your app uses one) + applyTheme(root); + return root; + } + + @state() + private denseLayout = false; + + protected override render() { + return html` + + + +
+
+ +
+
+ +
+
+ +
+
+
+ `; + } +} diff --git a/frontend/demo/component/dashboard/dashboard-editable.ts b/frontend/demo/component/dashboard/dashboard-editable.ts new file mode 100644 index 0000000000..498d377404 --- /dev/null +++ b/frontend/demo/component/dashboard/dashboard-editable.ts @@ -0,0 +1,209 @@ +import 'Frontend/demo/init'; // hidden-source-line +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.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` + + `; + } + + renderDashboard() { + return html` + + `; + } + + 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` + +
+
+ `, + 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` + // + // + // + // `; + // break; + // ... + // } + // + // render(widget, root); + } +} + +// end::snippet[] diff --git a/frontend/demo/component/dashboard/dashboard-internationalisation.ts b/frontend/demo/component/dashboard/dashboard-internationalisation.ts new file mode 100644 index 0000000000..0dfe448e60 --- /dev/null +++ b/frontend/demo/component/dashboard/dashboard-internationalisation.ts @@ -0,0 +1,27 @@ +import '@vaadin/dashboard/vaadin-dashboard-layout.js'; +import { html, LitElement } from 'lit'; +import type { DashboardI18n } from '@vaadin/dashboard'; + +// tag::snippet[] +const germanI18n: DashboardI18n = { + selectSection: 'Abschnitt auswählen', + selectWidget: 'Widget auswählen', + remove: 'Entfernen', + resize: 'Größe ändern', + resizeApply: 'Größenänderung anwenden', + resizeShrinkWidth: 'Breite verkleinern', + resizeGrowWidth: 'Breite vergrößern', + resizeShrinkHeight: 'Höhe verkleinern', + resizeGrowHeight: 'Höhe vergrößern', + move: 'Verschieben', + moveApply: 'Verschieben anwenden', + moveBackward: 'Nach hinten verschieben', + moveForward: 'Nach vorne verschieben', +}; + +export class Example extends LitElement { + render() { + return html` `; + } +} +// end::snippet[] diff --git a/frontend/demo/component/dashboard/dashboard-sections.ts b/frontend/demo/component/dashboard/dashboard-sections.ts new file mode 100644 index 0000000000..0fb2d27960 --- /dev/null +++ b/frontend/demo/component/dashboard/dashboard-sections.ts @@ -0,0 +1,62 @@ +import 'Frontend/demo/init'; // hidden-source-line +import '@vaadin/dashboard/vaadin-dashboard-layout.js'; +import '@vaadin/dashboard/vaadin-dashboard-section.js'; +import '@vaadin/dashboard/vaadin-dashboard-widget.js'; +import { html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { applyTheme } from 'Frontend/generated/theme'; + +@customElement('dashboard-sections') +export class Example extends LitElement { + protected override createRenderRoot() { + const root = super.createRenderRoot(); + // Apply custom theme (only supported if your app uses one) + applyTheme(root); + return root; + } + + render() { + return html` + + + + +
+
+ +
+
+ +
+
+
+ + + + +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ + `; + } +} diff --git a/frontend/demo/component/dashboard/dashboard-widget-contents.ts b/frontend/demo/component/dashboard/dashboard-widget-contents.ts new file mode 100644 index 0000000000..80de0282cf --- /dev/null +++ b/frontend/demo/component/dashboard/dashboard-widget-contents.ts @@ -0,0 +1,26 @@ +import 'Frontend/demo/init'; // hidden-source-line +import '@vaadin/dashboard/vaadin-dashboard-widget.js'; +import { html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { applyTheme } from 'Frontend/generated/theme'; + +@customElement('dashboard-widget-contents') +export class Example extends LitElement { + protected override createRenderRoot() { + const root = super.createRenderRoot(); + // Apply custom theme (only supported if your app uses one) + applyTheme(root); + return root; + } + + render() { + return html` + + + Widget content + Additional header content + + + `; + } +} diff --git a/frontend/demo/component/dashboard/react/dashboard-announcements.tsx b/frontend/demo/component/dashboard/react/dashboard-announcements.tsx new file mode 100644 index 0000000000..946927234d --- /dev/null +++ b/frontend/demo/component/dashboard/react/dashboard-announcements.tsx @@ -0,0 +1,135 @@ +import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line +import React, { useCallback } from 'react'; +import { useSignals } from '@preact/signals-react/runtime'; // hidden-source-line +import { useSignal } from '@vaadin/hilla-react-signals'; +import { + Dashboard, + type DashboardItemMovedEvent, + type DashboardItemMoveModeChangedEvent, + type DashboardItemRemovedEvent, + type DashboardItemResizedEvent, + type DashboardItemResizeModeChangedEvent, + type DashboardItemSelectedChangedEvent, + 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'; + +const widgetTitles: Record = { + [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', +}; + +function Example() { + useSignals(); // hidden-source-line + const widgets = useSignal([ + { 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 }, + ]); + const renderWidget = useCallback( + ({ item }: DashboardReactRendererProps) => ( + +
+ + ), + [] + ); + + // tag::snippet[] + const announcement = useSignal(''); + + function handleSelectedChange(e: DashboardItemSelectedChangedEvent) { + // This event is fired when the user starts or stops editing a widget + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const title = widgetTitles[(e.detail.item as WidgetConfig).type]; + const selected = e.detail.value ? 'selected' : 'unselected'; + + announcement.value = `Widget ${title} ${selected}`; + } + + function handleMoveModeChange(e: DashboardItemMoveModeChangedEvent) { + // This event is fired when the user enters or exits move mode + if (e.detail.value) { + announcement.value = 'Entered move mode'; + } else { + announcement.value = 'Exited move mode'; + } + } + + function handleResizeModeChange(e: DashboardItemResizeModeChangedEvent) { + // This event is fired when the user enters or exits resize mode + if (e.detail.value) { + announcement.value = 'Entered resize mode'; + } else { + announcement.value = 'Exited resize mode'; + } + } + + function handleMove(e: DashboardItemMovedEvent) { + // This event is fired when the user moves a widget + const index = e.detail.items.findIndex((widget) => widget === e.detail.item) + 1; + const total = e.detail.items.length; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const title = widgetTitles[(e.detail.item as WidgetConfig).type]; + + announcement.value = `Moved widget ${title} to position ${index} of ${total}`; + } + + function handleResize(e: DashboardItemResizedEvent) { + // This event is fired when the user resizes a widget + const colspan = e.detail.item.colspan; + const rowspan = e.detail.item.rowspan; + const title = widgetTitles[e.detail.item.type]; + + announcement.value = `Resized widget ${title} to ${colspan} columns, ${rowspan} rows`; + } + + function handleRemove(e: DashboardItemRemovedEvent) { + // This event is fired when the user removes a widget + const title = widgetTitles[(e.detail.item as WidgetConfig).type]; + + announcement.value = `Removed widget ${title}`; + } + + return ( + <> +

Live announcement:

+ {/* Live region for screen reader announcements. Changing its text content will result */} + {/* in a new announcement. This element is only visible for demonstration purposes. In */} + {/* your application you should visually hide it using CSS, for example by using the */} + {/* sr-only Lumo utility class: */} + {/*
{announcement}
*/} +
{announcement}
+ + {renderWidget} + + + ); + // end::snippet[] +} + +export default reactExample(Example); // hidden-source-line diff --git a/frontend/demo/component/dashboard/react/dashboard-basic.tsx b/frontend/demo/component/dashboard/react/dashboard-basic.tsx new file mode 100644 index 0000000000..388449fbf2 --- /dev/null +++ b/frontend/demo/component/dashboard/react/dashboard-basic.tsx @@ -0,0 +1,46 @@ +import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line +import React from 'react'; // hidden-source-line +import { DashboardLayout, DashboardWidget } from '@vaadin/react-components-pro'; + +function Example() { + return ( + // tag::snippet[] + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + // end::snippet[] + ); +} + +export default reactExample(Example); // hidden-source-line diff --git a/frontend/demo/component/dashboard/react/dashboard-editable.tsx b/frontend/demo/component/dashboard/react/dashboard-editable.tsx new file mode 100644 index 0000000000..8d32426985 --- /dev/null +++ b/frontend/demo/component/dashboard/react/dashboard-editable.tsx @@ -0,0 +1,186 @@ +import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line +import React, { useCallback, useEffect } from 'react'; +import { useSignals } from '@preact/signals-react/runtime'; // hidden-source-line +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.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() { + useSignals(); // hidden-source-line + const widgets = useSignal([]); + const editable = useSignal(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) => { + // 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 = ( + +
+ + ); + + // 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 ( + // + // + // + // ); + // ... + // } + 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 ( + <> + (e.detail.value as CustomMenuItem).action?.()} + /> + { + // 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} + + + ); +} + +// end::snippet[] + +export default reactExample(Example); // hidden-source-line diff --git a/frontend/demo/component/dashboard/react/dashboard-internationalisation.tsx b/frontend/demo/component/dashboard/react/dashboard-internationalisation.tsx new file mode 100644 index 0000000000..dddc193cb5 --- /dev/null +++ b/frontend/demo/component/dashboard/react/dashboard-internationalisation.tsx @@ -0,0 +1,28 @@ +import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line +import React from 'react'; // hidden-source-line +import { Dashboard, type DashboardI18n } from '@vaadin/react-components-pro'; + +// tag::snippet[] +const germanI18n: DashboardI18n = { + selectSection: 'Abschnitt auswählen', + selectWidget: 'Widget auswählen', + remove: 'Entfernen', + resize: 'Größe ändern', + resizeApply: 'Größenänderung anwenden', + resizeShrinkWidth: 'Breite verkleinern', + resizeGrowWidth: 'Breite vergrößern', + resizeShrinkHeight: 'Höhe verkleinern', + resizeGrowHeight: 'Höhe vergrößern', + move: 'Verschieben', + moveApply: 'Verschieben anwenden', + moveBackward: 'Nach hinten verschieben', + moveForward: 'Nach vorne verschieben', +}; + +function Example() { + return ; +} + +// end::snippet[] + +export default reactExample(Example); // hidden-source-line diff --git a/frontend/demo/component/dashboard/react/dashboard-sections.tsx b/frontend/demo/component/dashboard/react/dashboard-sections.tsx new file mode 100644 index 0000000000..d4a8ac621f --- /dev/null +++ b/frontend/demo/component/dashboard/react/dashboard-sections.tsx @@ -0,0 +1,53 @@ +import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line +import React from 'react'; // hidden-source-line +import { DashboardLayout, DashboardSection, DashboardWidget } from '@vaadin/react-components-pro'; + +function Example() { + return ( + // tag::snippet[] + + + +
+
+ +
+
+ +
+
+
+ {/* end::snippet[] */} + + + +
+
+ +
+
+ +
+
+ +
+
+
+ {/* tag::snippet[] */} +
+ // end::snippet[] + ); +} + +export default reactExample(Example); // hidden-source-line diff --git a/frontend/demo/component/dashboard/react/dashboard-widget-contents.tsx b/frontend/demo/component/dashboard/react/dashboard-widget-contents.tsx new file mode 100644 index 0000000000..1265bf9eb3 --- /dev/null +++ b/frontend/demo/component/dashboard/react/dashboard-widget-contents.tsx @@ -0,0 +1,16 @@ +import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line +import React from 'react'; // hidden-source-line +import { DashboardWidget } from '@vaadin/react-components-pro'; + +function Example() { + return ( + // tag::snippet[] + + Widget content + Additional header content + + // end::snippet[] + ); +} + +export default reactExample(Example); // hidden-source-line diff --git a/frontend/demo/services/DashboardService.ts b/frontend/demo/services/DashboardService.ts new file mode 100644 index 0000000000..66adaf60ae --- /dev/null +++ b/frontend/demo/services/DashboardService.ts @@ -0,0 +1,18 @@ +import type WidgetConfig from 'Frontend/generated/com/vaadin/demo/component/dashboard/WidgetConfig'; + +const key = 'dashboard-config'; + +class DashboardService { + // eslint-disable-next-line @typescript-eslint/require-await + async loadDashboard(): Promise { + const config = localStorage.getItem(key); + return config ? JSON.parse(config) : null; + } + + // eslint-disable-next-line @typescript-eslint/require-await + async saveDashboard(config: WidgetConfig[]): Promise { + localStorage.setItem(key, JSON.stringify(config)); + } +} + +export default new DashboardService(); diff --git a/frontend/demo/services/mocks.ts b/frontend/demo/services/mocks.ts index 160806f063..8dd97f0441 100644 --- a/frontend/demo/services/mocks.ts +++ b/frontend/demo/services/mocks.ts @@ -1,7 +1,8 @@ // This module should export all mock services used in live examples // During the build, the `Frontend/generated/endpoints` import is replaced with this module +import DashboardService from 'Frontend/demo/services/DashboardService'; import EmployeeService from 'Frontend/demo/services/EmployeeService'; import ProductService from 'Frontend/demo/services/ProductService'; export * from 'Frontend/generated/endpoints.js'; -export { EmployeeService, ProductService }; +export { DashboardService, EmployeeService, ProductService }; diff --git a/frontend/themes/docs/dashboard.css b/frontend/themes/docs/dashboard.css new file mode 100644 index 0000000000..c01c9f6360 --- /dev/null +++ b/frontend/themes/docs/dashboard.css @@ -0,0 +1,19 @@ +html { + --dashboard-widget-content-color: #e0e0e0; + --dashboard-widget-content-color-alt: #f5f5f5; +} + +html[theme~="dark"] { + --dashboard-widget-content-color: rgba(255, 255, 255, 0.1); + --dashboard-widget-content-color-alt: rgba(255, 255, 255, 0.2); +} + +.dashboard-widget-content { + height: 100%; + background: repeating-linear-gradient(45deg, var(--dashboard-widget-content-color), var(--dashboard-widget-content-color) 10px, var(--dashboard-widget-content-color-alt) 10px, var(--dashboard-widget-content-color-alt) 20px); + border-radius: var(--lumo-border-radius-m); + + &.small { + height: 50px; + } +} diff --git a/frontend/themes/docs/styles.css b/frontend/themes/docs/styles.css index bf282e1e77..b4e769974b 100644 --- a/frontend/themes/docs/styles.css +++ b/frontend/themes/docs/styles.css @@ -2,6 +2,7 @@ @import './basic-layouts.css'; @import './cookie-consent-theming.css'; +@import './dashboard.css'; @import './login-rich-content.css'; @import './notification-position-example.css'; @import './board.css'; diff --git a/src/main/java/com/vaadin/demo/component/dashboard/DashboardAnnouncements.java b/src/main/java/com/vaadin/demo/component/dashboard/DashboardAnnouncements.java new file mode 100644 index 0000000000..3fd6b38811 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/dashboard/DashboardAnnouncements.java @@ -0,0 +1,124 @@ +package com.vaadin.demo.component.dashboard; + +import com.vaadin.demo.DemoExporter; // hidden-source-line +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.html.Paragraph; +import com.vaadin.flow.router.Route; + +@Route("dashboard-announcements") +public class DashboardAnnouncements extends Div { + + public DashboardAnnouncements() { + add(new Paragraph("Live announcement:")); + + Dashboard dashboard = new Dashboard(); + dashboard.setEditable(true); + dashboard.setMinimumColumnWidth("150px"); + dashboard.setMaximumColumnCount(3); + + DashboardWidget visitors = new DashboardWidget("Visitors"); + visitors.setContent(createWidgetContent()); + dashboard.add(visitors); + + DashboardWidget downloads = new DashboardWidget("Downloads"); + downloads.setContent(createWidgetContent()); + dashboard.add(downloads); + + DashboardWidget conversions = new DashboardWidget("Conversions"); + conversions.setContent(createWidgetContent()); + dashboard.add(conversions); + + DashboardWidget visitorsByCountry = new DashboardWidget("Visitors by country"); + visitorsByCountry.setContent(createWidgetContent()); + visitorsByCountry.setRowspan(2); + dashboard.add(visitorsByCountry); + + DashboardWidget browserDistribution = new DashboardWidget("Browsers"); + browserDistribution.setContent(createWidgetContent()); + dashboard.add(browserDistribution); + + DashboardWidget catImage = new DashboardWidget("A kittykat!"); + catImage.setContent(createWidgetContent()); + dashboard.add(catImage); + + DashboardWidget visitorsByBrowser = new DashboardWidget("Visitors by browser"); + visitorsByBrowser.setContent(createWidgetContent()); + visitorsByBrowser.setColspan(2); + dashboard.add(visitorsByBrowser); + + // tag::snippet[] + // Live region for screen reader announcements. Changing its text content will result + // in a new announcement. This element is only visible for demonstration purposes. In + // your application you should visually hide it using CSS, for example by using the + // sr-only Lumo utility class: + // liveRegion.addClassName(LumoUtility.Accessibility.SCREEN_READER_ONLY) + Div liveRegion = new Div(); + liveRegion.getElement().setAttribute("aria-live", "polite"); + add(liveRegion); + + // This event is fired when the user starts or stops editing a widget + dashboard.addItemSelectedChangedListener(event -> { + String title = ((DashboardWidget)event.getItem()).getTitle(); + String selected = event.isSelected() ? "selected" : "unselected"; + + liveRegion.setText("Widget " + title + " " + selected); + }); + + // This event is fired when the user enters or exits move mode + dashboard.addItemMoveModeChangedListener(event -> { + if (event.isMoveMode()) { + liveRegion.setText("Entered move mode"); + } else { + liveRegion.setText("Exited move mode"); + } + }); + + // This event is fired when the user enters or exits resize mode + dashboard.addItemResizeModeChangedListener(event -> { + if (event.isResizeMode()) { + liveRegion.setText("Entered resize mode"); + } else { + liveRegion.setText("Exited resize mode"); + } + }); + + // This event is fired when the user moves a widget + dashboard.addItemMovedListener(event -> { + int index = event.getItems().indexOf(event.getItem()) + 1; + int total = event.getItems().size(); + String title = ((DashboardWidget)event.getItem()).getTitle(); + + liveRegion.setText("Moved widget " + title + " to position " + index + " of " + total); + }); + + // This event is fired when the user resizes a widget + dashboard.addItemResizedListener(event -> { + int colspan = event.getItem().getColspan(); + int rowspan = event.getItem().getRowspan(); + String title = event.getItem().getTitle(); + + liveRegion.setText("Resized widget " + title + " to " + colspan + " columns, " + rowspan + " rows"); + }); + + // This event is fired when the user removes a widget + dashboard.addItemRemovedListener(event -> { + String title = ((DashboardWidget)event.getItem()).getTitle(); + + liveRegion.setText("Removed widget " + title); + }); + // end::snippet[] + + add(dashboard); + } + + private Div createWidgetContent() { + Div content = new Div(); + content.setClassName("dashboard-widget-content"); + return content; + } + + public static class Exporter extends DemoExporter { // hidden-source-line + } // hidden-source-line +} diff --git a/src/main/java/com/vaadin/demo/component/dashboard/DashboardBasic.java b/src/main/java/com/vaadin/demo/component/dashboard/DashboardBasic.java new file mode 100644 index 0000000000..3b35b266c1 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/dashboard/DashboardBasic.java @@ -0,0 +1,60 @@ +package com.vaadin.demo.component.dashboard; + +import com.vaadin.demo.DemoExporter; // hidden-source-line +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.router.Route; + +@Route("dashboard-basic") +public class DashboardBasic extends Div { + + public DashboardBasic() { + // tag::snippet[] + Dashboard dashboard = new Dashboard(); + dashboard.setMinimumColumnWidth("150px"); + dashboard.setMaximumColumnCount(3); + + DashboardWidget visitors = new DashboardWidget("Visitors"); + visitors.setContent(createWidgetContent()); + dashboard.add(visitors); + + DashboardWidget downloads = new DashboardWidget("Downloads"); + downloads.setContent(createWidgetContent()); + dashboard.add(downloads); + + DashboardWidget conversions = new DashboardWidget("Conversions"); + conversions.setContent(createWidgetContent()); + dashboard.add(conversions); + + DashboardWidget visitorsByCountry = new DashboardWidget("Visitors by country"); + visitorsByCountry.setContent(createWidgetContent()); + visitorsByCountry.setRowspan(2); + dashboard.add(visitorsByCountry); + + DashboardWidget browserDistribution = new DashboardWidget("Browsers"); + browserDistribution.setContent(createWidgetContent()); + dashboard.add(browserDistribution); + + DashboardWidget catImage = new DashboardWidget("A kittykat!"); + catImage.setContent(createWidgetContent()); + dashboard.add(catImage); + + DashboardWidget visitorsByBrowser = new DashboardWidget("Visitors by browser"); + visitorsByBrowser.setContent(createWidgetContent()); + visitorsByBrowser.setColspan(2); + dashboard.add(visitorsByBrowser); + + add(dashboard); + // end::snippet[] + } + + private Div createWidgetContent() { + Div content = new Div(); + content.setClassName("dashboard-widget-content"); + return content; + } + + public static class Exporter extends DemoExporter { // hidden-source-line + } // hidden-source-line +} diff --git a/src/main/java/com/vaadin/demo/component/dashboard/DashboardEditable.java b/src/main/java/com/vaadin/demo/component/dashboard/DashboardEditable.java new file mode 100644 index 0000000000..edefc463a4 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/dashboard/DashboardEditable.java @@ -0,0 +1,186 @@ +package com.vaadin.demo.component.dashboard; + +import com.vaadin.demo.DemoExporter; // hidden-source-line +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 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 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 dashboardConfig = dashboardStorage.load(); + if (dashboardConfig == null) { + dashboardConfig = defaultConfig; + } + + applyConfiguration(dashboardConfig); + } + + private void applyConfiguration(List 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; + } + + public static class Exporter extends DemoExporter { // hidden-source-line + } // hidden-source-line +} diff --git a/src/main/java/com/vaadin/demo/component/dashboard/DashboardInternationalisation.java b/src/main/java/com/vaadin/demo/component/dashboard/DashboardInternationalisation.java new file mode 100644 index 0000000000..ffbba497df --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/dashboard/DashboardInternationalisation.java @@ -0,0 +1,29 @@ +package com.vaadin.demo.component.dashboard; + +import com.vaadin.flow.component.dashboard.Dashboard; +import com.vaadin.flow.component.html.Div; + +public class DashboardInternationalisation extends Div { + public DashboardInternationalisation() { + Dashboard dashboard = new Dashboard(); + // tag::snippet[] + Dashboard.DashboardI18n germanI18n = new Dashboard.DashboardI18n(); + germanI18n.setSelectSection("Abschnitt auswählen"); + germanI18n.setSelectWidget("Widget auswählen"); + germanI18n.setRemove("Entfernen"); + germanI18n.setResize("Größe ändern"); + germanI18n.setResizeApply("Größenänderung anwenden"); + germanI18n.setResizeShrinkWidth("Breite verkleinern"); + germanI18n.setResizeGrowWidth("Breite vergrößern"); + germanI18n.setResizeShrinkHeight("Höhe verkleinern"); + germanI18n.setResizeGrowHeight("Höhe vergrößern"); + germanI18n.setMove("Verschieben"); + germanI18n.setMoveApply("Verschieben anwenden"); + germanI18n.setMoveBackward("Nach hinten verschieben"); + germanI18n.setMoveForward("Nach vorne verschieben"); + + dashboard.setI18n(germanI18n); + // end::snippet[] + add(dashboard); + } +} diff --git a/src/main/java/com/vaadin/demo/component/dashboard/DashboardSections.java b/src/main/java/com/vaadin/demo/component/dashboard/DashboardSections.java new file mode 100644 index 0000000000..99cc223ea5 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/dashboard/DashboardSections.java @@ -0,0 +1,64 @@ +package com.vaadin.demo.component.dashboard; + +import com.vaadin.demo.DemoExporter; // hidden-source-line +import com.vaadin.flow.component.dashboard.Dashboard; +import com.vaadin.flow.component.dashboard.DashboardSection; +import com.vaadin.flow.component.dashboard.DashboardWidget; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; + +@Route("dashboard-sections") +public class DashboardSections extends Div { + + public DashboardSections() { + Dashboard dashboard = new Dashboard(); + dashboard.setMinimumColumnWidth("150px"); + dashboard.setMaximumColumnCount(3); + + // tag::snippet[] + DashboardSection statsSection = dashboard.addSection("Monthly Funnel Stats"); + + DashboardWidget visitors = new DashboardWidget("Visitors"); + visitors.setContent(createWidgetContent()); + statsSection.add(visitors); + + DashboardWidget downloads = new DashboardWidget("Downloads"); + downloads.setContent(createWidgetContent()); + statsSection.add(downloads); + + DashboardWidget conversions = new DashboardWidget("Conversions"); + conversions.setContent(createWidgetContent()); + statsSection.add(conversions); + // end::snippet[] + + DashboardSection detailsSection = dashboard.addSection("Visitor Details"); + DashboardWidget visitorsByCountry = new DashboardWidget("Visitors by country"); + visitorsByCountry.setContent(createWidgetContent()); + visitorsByCountry.setRowspan(2); + detailsSection.add(visitorsByCountry); + + DashboardWidget browserDistribution = new DashboardWidget("Browsers"); + browserDistribution.setContent(createWidgetContent()); + detailsSection.add(browserDistribution); + + DashboardWidget catImage = new DashboardWidget("A kittykat!"); + catImage.setContent(createWidgetContent()); + detailsSection.add(catImage); + + DashboardWidget visitorsByBrowser = new DashboardWidget("Visitors by browser"); + visitorsByBrowser.setContent(createWidgetContent()); + visitorsByBrowser.setColspan(2); + detailsSection.add(visitorsByBrowser); + + add(dashboard); + } + + private Div createWidgetContent() { + Div content = new Div(); + content.setClassName("dashboard-widget-content"); + return content; + } + + public static class Exporter extends DemoExporter { // hidden-source-line + } // hidden-source-line +} diff --git a/src/main/java/com/vaadin/demo/component/dashboard/DashboardService.java b/src/main/java/com/vaadin/demo/component/dashboard/DashboardService.java new file mode 100644 index 0000000000..23acec1ce6 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/dashboard/DashboardService.java @@ -0,0 +1,29 @@ +package com.vaadin.demo.component.dashboard; + +import com.vaadin.hilla.BrowserCallable; +import com.vaadin.hilla.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[] diff --git a/src/main/java/com/vaadin/demo/component/dashboard/DashboardStorage.java b/src/main/java/com/vaadin/demo/component/dashboard/DashboardStorage.java new file mode 100644 index 0000000000..195fb90db2 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/dashboard/DashboardStorage.java @@ -0,0 +1,22 @@ +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 config; + + public List load() { + return config; + } + + public void save(List config) { + this.config = config; + } +} +// end::snippet[] diff --git a/src/main/java/com/vaadin/demo/component/dashboard/DashboardWidgetContents.java b/src/main/java/com/vaadin/demo/component/dashboard/DashboardWidgetContents.java new file mode 100644 index 0000000000..c917aacc3d --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/dashboard/DashboardWidgetContents.java @@ -0,0 +1,24 @@ +package com.vaadin.demo.component.dashboard; + +import com.vaadin.demo.DemoExporter; // hidden-source-line +import com.vaadin.flow.component.dashboard.DashboardWidget; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.Route; + +@Route("dashboard-widget-contents") +public class DashboardWidgetContents extends Div { + + public DashboardWidgetContents() { + // tag::snippet[] + DashboardWidget widget = new DashboardWidget("Widget title"); + widget.setContent(new Span("Widget content")); + widget.setHeaderContent(new Span("Additional header content")); + // end::snippet[] + + add(widget); + } + + public static class Exporter extends DemoExporter { // hidden-source-line + } // hidden-source-line +} diff --git a/src/main/java/com/vaadin/demo/component/dashboard/WidgetConfig.java b/src/main/java/com/vaadin/demo/component/dashboard/WidgetConfig.java new file mode 100644 index 0000000000..fe5bce5a43 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/dashboard/WidgetConfig.java @@ -0,0 +1,70 @@ +package com.vaadin.demo.component.dashboard; + +import com.vaadin.hilla.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[] diff --git a/src/main/resources/vaadin-featureflags.properties b/src/main/resources/vaadin-featureflags.properties index 5b765ae3ff..c2a7e1b33f 100644 --- a/src/main/resources/vaadin-featureflags.properties +++ b/src/main/resources/vaadin-featureflags.properties @@ -1 +1 @@ -com.vaadin.experimental.mapComponent=true \ No newline at end of file +com.vaadin.experimental.dashboardComponent=true \ No newline at end of file