A comprehensive dashboard system with reusable Angular libraries and Spring Boot Java API. Build customizable, drag-and-drop dashboards with minimal effort.
reusable-dashboard-components/
├── angular/ # Nx monorepo
│ ├── libs/dashboard/
│ │ ├── core/ # @expeed/ngx-dashboard-core - Services, models, config
│ │ └── widgets/ # @expeed/ngx-dashboard-widgets - Widget components
│ └── apps/demo/ # Demo Angular application
├── api/ # Gradle multi-module
│ ├── dashboard-core/ # Reusable Spring Boot library
│ └── dashboard-demo/ # Demo API with sample data
└── database/
└── liquibase/ # Shared database migrations
- Widget Types: KPI, Chart, Table, Activity Feed (extensible)
- GridStack Integration: Drag-and-drop widget positioning and resizing
- Template System: Admin-managed dashboard templates
- Role-based Access: Template assignment by user roles
- Auto-provisioning: Automatic dashboard creation for new users
- Dynamic Data Sources: SQL, API, and static data support
- Edit Mode: Toggle between view and edit modes
- Responsive Grid: 12-column grid system with configurable cell height
- Java 21+ (for Spring Boot API)
- Node.js 20+ (for Angular)
- PostgreSQL 15+ (for database)
- Docker (optional, for local development)
docker-compose up -dThis starts PostgreSQL on localhost:5432 with database dashboard_demo.
cd api
./gradlew :dashboard-demo:bootRunAPI starts on http://localhost:8080
cd angular
npm install
npm startApp starts on http://localhost:4200
| Role | Password | |
|---|---|---|
| Admin | admin@example.com | admin123 |
| Manager | manager@example.com | manager123 |
| User | user@example.com | user123 |
# Install the Angular libraries
npm install @expeed/ngx-dashboard-core @expeed/ngx-dashboard-widgets gridstack// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideDashboard } from '@expeed/ngx-dashboard-core';
export const appConfig: ApplicationConfig = {
providers: [
provideDashboard({
apiUrl: '/api',
gridColumns: 12,
gridCellHeight: 80,
enableEditMode: true,
enableWidgetResize: true,
enableWidgetFloat: true, // Widgets stay in place, don't auto-sink
theme: 'light'
})
]
};// dashboard-page.component.ts
import { Component } from '@angular/core';
import { DynamicDashboardComponent } from '@expeed/ngx-dashboard-widgets';
@Component({
selector: 'app-dashboard-page',
standalone: true,
imports: [DynamicDashboardComponent],
template: `
<dash-dynamic-dashboard
[dashboardId]="dashboardId"
(dashboardLoaded)="onDashboardLoaded($event)"
(layoutChanged)="onLayoutChanged($event)"
(widgetAdded)="onWidgetAdded($event)"
(widgetRemoved)="onWidgetRemoved($event)">
</dash-dynamic-dashboard>
`
})
export class DashboardPageComponent {
dashboardId = 'your-dashboard-uuid';
onDashboardLoaded(dashboard: Dashboard) {
console.log('Dashboard loaded:', dashboard);
}
onLayoutChanged(positions: WidgetPosition[]) {
console.log('Layout changed:', positions);
}
onWidgetAdded(widget: DashboardWidget) {
console.log('Widget added:', widget);
}
onWidgetRemoved(widgetId: string) {
console.log('Widget removed:', widgetId);
}
}// styles.scss
@import 'gridstack/dist/gridstack.min.css';| Widget Type | Description | Data Format |
|---|---|---|
KPI |
Single metric display with icon and trend | { value: number, label: string, icon?: string, trend?: number } |
CHART |
Bar, line, pie charts (Chart.js) | { labels: string[], datasets: [...] } |
TABLE |
Paginated data table | { columns: [...], rows: [...], total: number } |
ACTIVITY_FEED |
Timeline of recent activities | { items: [{ title, description, timestamp, icon }] } |
// gauge-widget.component.ts
import { Component, Input } from '@angular/core';
import { BaseWidgetComponent } from '@expeed/ngx-dashboard-widgets';
@Component({
selector: 'app-gauge-widget',
standalone: true,
template: `
<div class="gauge-container">
<svg viewBox="0 0 100 50">
<!-- Gauge arc -->
<path d="M10,50 A40,40 0 0,1 90,50"
fill="none"
stroke="#e0e0e0"
stroke-width="8"/>
<path d="M10,50 A40,40 0 0,1 90,50"
fill="none"
stroke="#3b82f6"
stroke-width="8"
[attr.stroke-dasharray]="dashArray"/>
</svg>
<div class="gauge-value">{{ data?.value }}%</div>
<div class="gauge-label">{{ data?.label }}</div>
</div>
`,
styles: [`
.gauge-container { text-align: center; padding: 1rem; }
.gauge-value { font-size: 2rem; font-weight: bold; }
.gauge-label { color: #666; }
`]
})
export class GaugeWidgetComponent extends BaseWidgetComponent {
get dashArray(): string {
const percentage = this.data?.value || 0;
const circumference = 126; // Half circle
return `${(percentage / 100) * circumference} ${circumference}`;
}
}// app.config.ts
import { provideWidgetRegistry } from '@expeed/ngx-dashboard-widgets';
import { GaugeWidgetComponent } from './widgets/gauge-widget.component';
export const appConfig: ApplicationConfig = {
providers: [
provideDashboard({ apiUrl: '/api' }),
provideWidgetRegistry([
{ type: 'GAUGE', component: GaugeWidgetComponent }
])
]
};INSERT INTO widget_definition (id, name, display_name, widget_type, data_source_type, default_width, default_height)
VALUES ('gauge-widget', 'gauge', 'Gauge Chart', 'GAUGE', 'SQL', 4, 2);| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/login |
Login and get JWT token |
| GET | /api/dashboards |
List user dashboards |
| GET | /api/dashboards/{id} |
Get dashboard with widgets |
| POST | /api/dashboards |
Create dashboard |
| PUT | /api/dashboards/{id} |
Update dashboard |
| DELETE | /api/dashboards/{id} |
Delete dashboard |
| PUT | /api/dashboards/{id}/layout |
Save widget positions |
| POST | /api/dashboards/{id}/widgets |
Add widget |
| PUT | /api/dashboards/{id}/widgets/{wid} |
Update widget |
| DELETE | /api/dashboards/{id}/widgets/{wid} |
Remove widget |
| POST | /api/dashboards/{id}/widgets/{wid}/data |
Get widget data |
| GET | /api/widget-definitions |
List available widget types |
| GET | /api/dashboard-templates |
List templates |
| POST | /api/dashboard-templates/{id}/provision |
Create dashboard from template |
import { DashboardService } from '@expeed/ngx-dashboard-core';
@Component({ ... })
export class MyComponent {
private dashboardService = inject(DashboardService);
// Load a dashboard
loadDashboard(id: string) {
this.dashboardService.loadDashboard(id).subscribe(dashboard => {
console.log(dashboard);
});
}
// Toggle edit mode
toggleEdit() {
this.dashboardService.toggleEditMode();
}
// Check current state
get isEditing() {
return this.dashboardService.editMode();
}
get currentDashboard() {
return this.dashboardService.currentDashboard();
}
}// build.gradle.kts
dependencies {
implementation(project(":dashboard-core"))
// or from Maven
// implementation("com.expeed:dashboard-core:1.0.0")
}# application.yml
dashboard:
context:
tenant-id: ${TENANT_ID:default}@RestController
public class MyController {
@Autowired
private DashboardService dashboardService;
@Autowired
private DashboardContext dashboardContext;
@GetMapping("/my-dashboards")
public List<Dashboard> getMyDashboards() {
String userId = dashboardContext.getUserId();
return dashboardService.getDashboardsForUser(userId);
}
}@Component
public class CustomWidgetDataProvider implements WidgetDataProvider {
@Override
public boolean supports(String dataSourceType) {
return "CUSTOM_API".equals(dataSourceType);
}
@Override
public WidgetDataResponse getData(WidgetDefinition definition, Map<String, Object> params) {
// Fetch data from external API
var data = externalApiClient.fetchData(definition.getDataSourceConfig());
return new WidgetDataResponse(data);
}
}| Option | Type | Default | Description |
|---|---|---|---|
apiUrl |
string | /api |
Base URL for API calls |
gridColumns |
number | 12 |
Number of grid columns |
gridCellHeight |
number | 80 |
Height of each grid cell in pixels |
enableEditMode |
boolean | true |
Allow users to edit dashboards |
enableWidgetResize |
boolean | true |
Allow widget resizing |
enableWidgetFloat |
boolean | true |
Widgets stay in place (don't auto-compact) |
defaultRefreshInterval |
number | 60 |
Auto-refresh interval in seconds |
theme |
string | light |
Theme: light, dark, or auto |
| Variable | Default | Description |
|---|---|---|
DB_HOST |
localhost | PostgreSQL host |
DB_PORT |
5432 | PostgreSQL port |
DB_NAME |
dashboard_demo | Database name |
DB_USERNAME |
postgres | Database user |
DB_PASSWORD |
postgres | Database password |
JWT_SECRET |
(generated) | JWT signing secret |
# API tests (uses Testcontainers)
cd api
./gradlew test
# Angular tests
cd angular
npm test# Build API
cd api
./gradlew :dashboard-demo:build
# Build Angular libraries
cd angular
npm run build:libs
# Build Angular demo app
cd angular
npm run build# Angular
npm start # Start dev server
npm test # Run tests
npm run build # Production build
npm run build:libs # Build libraries only
npm run lint # Lint code
# API
./gradlew bootRun # Start dev server
./gradlew test # Run tests
./gradlew build # Production build
./gradlew :dashboard-core:publish # Publish libraryEnsure enableWidgetFloat: true in your dashboard config. This prevents widgets from auto-sinking when others are moved.
Import the GridStack CSS in your global styles:
@import 'gridstack/dist/gridstack.min.css';Configure CORS in your Spring Boot application:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:4200")
.allowedMethods("*");
}
}MIT