ui-dashboard-composer-tool.ts•51.3 kB
/**
* UI Dashboard Composer Tool
* Creates interactive KPI dashboards with widgets, charts, and real-time updates
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SAPClient } from "../../services/sap-client.js";
import { Logger } from "../../utils/logger.js";
import { UIComponentLibrary } from "../../ui/components/ui-component-library.js";
import { IntelligentToolRouter } from "../../middleware/intelligent-tool-router.js";
import { SecureErrorHandler } from "../../utils/secure-error-handler.js";
import {
DashboardConfig,
WidgetConfig,
DataSourceMapping,
LayoutDefinition,
UIRenderResult
} from "../../ui/types/ui-types.js";
import { z } from "zod";
const UIDashboardComposerSchema = {
title: z.string().describe("Dashboard title"),
description: z.string().optional().describe("Dashboard description"),
layout: z.object({
type: z.enum(['grid', 'flexbox', 'absolute']).describe("Layout type"),
columns: z.number().min(1).max(12).optional().describe("Number of columns for grid layout"),
gap: z.string().optional().describe("Gap between widgets (e.g., '1rem')"),
responsive: z.boolean().optional().describe("Enable responsive design")
}).describe("Dashboard layout configuration"),
widgets: z.array(z.object({
id: z.string().describe("Unique widget identifier"),
type: z.enum(['kpi-card', 'chart', 'table', 'list', 'gauge', 'timeline', 'map', 'custom']).describe("Widget type"),
title: z.string().describe("Widget title"),
position: z.object({
row: z.number().min(0).describe("Grid row position"),
col: z.number().min(0).describe("Grid column position"),
width: z.number().min(1).max(12).describe("Widget width (grid columns)"),
height: z.number().min(1).describe("Widget height (grid rows)")
}).describe("Widget position and size"),
config: z.record(z.any()).optional().describe("Widget-specific configuration"),
dataSource: z.object({
entitySet: z.string().describe("SAP entity set for data"),
query: z.object({
filter: z.string().optional(),
select: z.string().optional(),
orderby: z.string().optional(),
top: z.number().optional()
}).optional().describe("OData query parameters"),
aggregation: z.object({
groupBy: z.array(z.string()).optional().describe("Fields to group by"),
measures: z.array(z.object({
field: z.string(),
operation: z.enum(['sum', 'avg', 'count', 'min', 'max'])
})).optional().describe("Aggregation measures")
}).optional().describe("Data aggregation configuration"),
refresh: z.number().optional().describe("Refresh interval in seconds")
}).describe("Data source configuration")
})).describe("Dashboard widgets"),
datasources: z.array(z.object({
id: z.string().describe("Data source identifier"),
entitySet: z.string().describe("SAP entity set"),
query: z.string().optional().describe("Custom OData query"),
cacheTtl: z.number().optional().describe("Cache TTL in seconds"),
transform: z.string().optional().describe("Data transformation function name")
})).optional().describe("Shared data sources"),
refreshInterval: z.number().min(5).max(3600).optional().describe("Global refresh interval in seconds"),
theme: z.enum(['sap_horizon', 'sap_fiori_3', 'dark', 'light']).optional().describe("Dashboard theme"),
filters: z.array(z.object({
field: z.string().describe("Filter field name"),
label: z.string().describe("Filter display label"),
type: z.enum(['select', 'daterange', 'text', 'number']).describe("Filter type"),
options: z.array(z.object({
value: z.string(),
label: z.string()
})).optional().describe("Options for select filters"),
defaultValue: z.any().optional().describe("Default filter value")
})).optional().describe("Global dashboard filters"),
exportOptions: z.object({
pdf: z.boolean().optional(),
excel: z.boolean().optional(),
powerpoint: z.boolean().optional(),
image: z.boolean().optional()
}).optional().describe("Export format options")
};
export class UIDashboardComposerTool {
private mcpServer: McpServer;
private sapClient: SAPClient;
private logger: Logger;
private componentLibrary: UIComponentLibrary;
private intelligentRouter: IntelligentToolRouter;
private errorHandler: SecureErrorHandler;
constructor(
mcpServer: McpServer,
sapClient: SAPClient,
logger: Logger
) {
this.mcpServer = mcpServer;
this.sapClient = sapClient;
this.logger = logger;
this.componentLibrary = new UIComponentLibrary();
this.intelligentRouter = new IntelligentToolRouter();
this.errorHandler = new SecureErrorHandler(logger);
}
public async register(): Promise<void> {
this.mcpServer.registerTool(
"ui-dashboard-composer",
{
title: "UI Dashboard Composer",
description: `Create interactive KPI dashboards with widgets, charts, and real-time updates.
Features:
- Multiple widget types (KPI cards, charts, tables, gauges, timelines)
- Flexible grid-based layout system
- Real-time data refresh and updates
- Interactive filters and drill-down capabilities
- Chart.js integration for advanced visualizations
- Export to PDF, Excel, PowerPoint, and images
- Responsive design for mobile and desktop
- SAP Fiori design language compliance
- Custom aggregations and calculations
- WebSocket support for live data
Required scope: ui.dashboards
Widget Types:
- kpi-card: Key performance indicator cards
- chart: Bar, line, pie, doughnut, and radar charts
- table: Data tables with sorting and filtering
- list: Simple list displays
- gauge: Circular and linear progress gauges
- timeline: Event timelines and process flows
- map: Geographic data visualization
- custom: Custom HTML/JavaScript widgets
Examples:
- Sales dashboard: {"title": "Sales Overview", "widgets": [...]}
- Executive summary: {"title": "Executive KPIs", "layout": {"type": "grid", "columns": 3}}
- Real-time monitoring: {"refreshInterval": 30, "widgets": [...]}`,
inputSchema: UIDashboardComposerSchema
},
async (args: Record<string, unknown>) => {
return await this.handleDashboardComposition(args);
}
);
this.logger.info("✅ UI Dashboard Composer tool registered successfully");
}
private async handleDashboardComposition(args: unknown): Promise<any> {
try {
// Validate input parameters
const params = z.object(UIDashboardComposerSchema).parse(args);
this.logger.info(`📊 Generating dashboard: ${params.title} with ${params.widgets.length} widgets`);
// Check authentication and authorization
const authCheck = await this.checkUIAccess('ui.dashboards');
if (!authCheck.hasAccess) {
return {
content: [{
type: "text",
text: `❌ Authorization denied: ${authCheck.reason || 'Access denied for UI dashboard generation'}\n\nRequired scope: ui.dashboards`
}]
};
}
// Step 1: Validate and prepare widget configurations
const validatedWidgets = await this.validateAndPrepareWidgets(params.widgets);
// Step 2: Prepare data sources
const dataSources = await this.prepareDashboardDataSources(validatedWidgets, params.datasources);
// Step 3: Create dashboard layout
const layoutDefinition: LayoutDefinition = {
type: params.layout.type,
config: {
columns: params.layout.columns || 4,
gap: params.layout.gap || '1rem',
responsive: params.layout.responsive !== false ? {
breakpoints: {
mobile: { columns: 1, gap: '0.5rem' },
tablet: { columns: 2, gap: '1rem' },
desktop: { columns: 4, gap: '1rem' }
}
} : undefined
},
components: this.createWidgetComponents(validatedWidgets)
};
// Step 4: Create dashboard configuration
const dashboardConfig: DashboardConfig = {
layout: layoutDefinition,
widgets: validatedWidgets,
datasources: dataSources,
refreshInterval: params.refreshInterval || 60,
theme: params.theme || 'sap_horizon'
};
// Step 5: Generate dashboard UI
const dashboardResult = await this.componentLibrary.generateDashboard(dashboardConfig);
// Step 6: Add SAP-specific enhancements
const enhancedResult = await this.enhanceDashboardResult(dashboardResult, params);
// Step 7: Prepare response
const response = this.createDashboardResponse(enhancedResult, params, dataSources);
this.logger.info(`✅ Dashboard '${params.title}' generated successfully`);
return {
content: [
{
type: "text",
text: `# ${params.title}\n\n` +
`${params.description || 'Interactive SAP dashboard with real-time KPIs'}\n\n` +
`## Dashboard Overview:\n` +
`- Widgets: ${params.widgets.length}\n` +
`- Layout: ${params.layout.type} (${params.layout.columns || 4} columns)\n` +
`- Theme: ${params.theme || 'sap_horizon'}\n` +
`- Refresh: Every ${params.refreshInterval || 60} seconds\n` +
`- Filters: ${params.filters?.length || 0}\n\n` +
`## Widget Types:\n` +
params.widgets.map(w => `- ${w.type}: ${w.title}`).join('\n') + '\n\n' +
`## Data Sources:\n` +
`- Entity Sets: ${Array.from(new Set(params.widgets.map(w => w.dataSource.entitySet))).join(', ')}\n` +
`- Real-time Updates: ${params.refreshInterval ? '✅' : '❌'}\n\n` +
`## Features:\n` +
`- Export Options: ${Object.entries(params.exportOptions || {}).filter(([k, v]) => v).map(([k]) => k.toUpperCase()).join(', ') || 'Basic'}\n` +
`- Responsive Design: ✅\n` +
`- Interactive Filters: ${params.filters?.length ? '✅' : '❌'}\n\n` +
`Embed this dashboard in your SAP application or use via MCP client.`
},
{
type: "resource",
data: response,
mimeType: "application/json"
}
]
};
} catch (error) {
this.logger.error(`❌ Failed to generate dashboard`, error as Error);
return {
content: [{
type: "text",
text: `❌ Failed to generate dashboard: ${(error as Error).message}`
}]
};
}
}
/**
* Validate and prepare widget configurations
*/
private async validateAndPrepareWidgets(widgets: any[]): Promise<WidgetConfig[]> {
const validatedWidgets: WidgetConfig[] = [];
for (const widget of widgets) {
// Fetch mock data for the widget
const widgetData = await this.fetchWidgetData(widget);
const validatedWidget: WidgetConfig = {
id: widget.id,
type: widget.type,
title: widget.title,
position: widget.position,
config: {
...widget.config,
data: widgetData,
theme: widget.config?.theme || 'sap_horizon'
},
dataKey: widget.dataSource.entitySet
};
validatedWidgets.push(validatedWidget);
}
return validatedWidgets;
}
/**
* Fetch widget data based on configuration
*/
private async fetchWidgetData(widget: any): Promise<any> {
const entitySet = widget.dataSource.entitySet;
const query = widget.dataSource.query || {};
try {
switch (widget.type) {
case 'kpi-card':
return this.generateKPIData(entitySet, widget.config);
case 'chart':
return this.generateChartData(entitySet, widget.config, query);
case 'table':
return this.generateTableData(entitySet, query);
case 'gauge':
return this.generateGaugeData(entitySet, widget.config);
case 'timeline':
return this.generateTimelineData(entitySet, query);
default:
return this.generateGenericData(entitySet, query);
}
} catch (error) {
this.logger.error(`Failed to fetch data for widget ${widget.id}`, error as Error);
return null;
}
}
/**
* Generate KPI card data
*/
private generateKPIData(entitySet: string, config: any): any {
const kpiTemplates: { [key: string]: any } = {
Customers: {
totalCustomers: { value: 1247, trend: '+5.2%', status: 'positive' },
newCustomers: { value: 89, trend: '+12.3%', status: 'positive' },
activeCustomers: { value: 1156, trend: '-2.1%', status: 'negative' },
customerSatisfaction: { value: 87.5, trend: '+3.4%', status: 'positive', unit: '%' }
},
Orders: {
totalOrders: { value: 3456, trend: '+8.7%', status: 'positive' },
pendingOrders: { value: 234, trend: '-15.2%', status: 'positive' },
completedOrders: { value: 3222, trend: '+9.1%', status: 'positive' },
averageOrderValue: { value: 847.50, trend: '+4.3%', status: 'positive', unit: '$' }
},
Products: {
totalProducts: { value: 567, trend: '+2.1%', status: 'positive' },
lowStock: { value: 23, trend: '+45.2%', status: 'warning' },
topSelling: { value: 89, trend: '+15.7%', status: 'positive' },
outOfStock: { value: 5, trend: '-28.6%', status: 'positive' }
}
};
const entityKPIs = kpiTemplates[entitySet] || {
total: { value: Math.floor(Math.random() * 10000), trend: '+5.2%', status: 'positive' },
active: { value: Math.floor(Math.random() * 8000), trend: '+3.1%', status: 'positive' }
};
const kpiName = config?.kpiType || Object.keys(entityKPIs)[0];
return entityKPIs[kpiName] || entityKPIs[Object.keys(entityKPIs)[0]];
}
/**
* Generate chart data
*/
private generateChartData(entitySet: string, config: any, query: any): any {
const chartType = config?.chartType || 'bar';
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
switch (chartType) {
case 'line':
return {
type: 'line',
data: {
labels: months.slice(0, 6),
datasets: [{
label: `${entitySet} Trend`,
data: Array.from({ length: 6 }, () => Math.floor(Math.random() * 1000) + 500),
borderColor: '#0070f2',
backgroundColor: 'rgba(0, 112, 242, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
plugins: {
title: { display: true, text: `${entitySet} Over Time` }
}
}
};
case 'pie':
const categories = entitySet === 'Products' ?
['Electronics', 'Clothing', 'Books', 'Home & Garden', 'Sports'] :
['Category A', 'Category B', 'Category C', 'Category D', 'Category E'];
return {
type: 'pie',
data: {
labels: categories,
datasets: [{
data: Array.from({ length: categories.length }, () => Math.floor(Math.random() * 300) + 100),
backgroundColor: ['#0070f2', '#52c41a', '#faad14', '#f5222d', '#722ed1']
}]
},
options: {
responsive: true,
plugins: {
title: { display: true, text: `${entitySet} Distribution` }
}
}
};
case 'bar':
default:
return {
type: 'bar',
data: {
labels: months.slice(0, 8),
datasets: [{
label: entitySet,
data: Array.from({ length: 8 }, () => Math.floor(Math.random() * 800) + 200),
backgroundColor: '#0070f2',
borderColor: '#0058d3',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
title: { display: true, text: `${entitySet} by Month` }
},
scales: {
y: { beginAtZero: true }
}
}
};
}
}
/**
* Generate table data
*/
private generateTableData(entitySet: string, query: any): any {
const tableData = {
headers: this.getTableHeaders(entitySet),
rows: this.generateTableRows(entitySet, query.top || 10)
};
return tableData;
}
/**
* Get table headers for entity set
*/
private getTableHeaders(entitySet: string): string[] {
const headerMappings: { [key: string]: string[] } = {
Customers: ['Customer ID', 'Company Name', 'Country', 'Revenue', 'Status'],
Orders: ['Order ID', 'Customer', 'Date', 'Amount', 'Status'],
Products: ['Product ID', 'Name', 'Category', 'Price', 'Stock']
};
return headerMappings[entitySet] || ['ID', 'Name', 'Status', 'Date'];
}
/**
* Generate table rows
*/
private generateTableRows(entitySet: string, count: number): any[][] {
const rows: any[][] = [];
for (let i = 1; i <= count; i++) {
let row: any[];
switch (entitySet) {
case 'Customers':
row = [
`CUST${i.toString().padStart(4, '0')}`,
`Company ${i}`,
['USA', 'Germany', 'UK', 'France'][i % 4],
`$${(Math.random() * 100000).toFixed(0)}`,
['Active', 'Inactive', 'Pending'][i % 3]
];
break;
case 'Orders':
row = [
`ORD${i.toString().padStart(6, '0')}`,
`Customer ${i}`,
new Date(2024, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toLocaleDateString(),
`$${(Math.random() * 5000 + 100).toFixed(2)}`,
['Open', 'Processing', 'Shipped', 'Delivered'][i % 4]
];
break;
case 'Products':
row = [
`PROD${i.toString().padStart(4, '0')}`,
`Product ${i}`,
['Electronics', 'Clothing', 'Books', 'Home'][i % 4],
`$${(Math.random() * 500 + 10).toFixed(2)}`,
Math.floor(Math.random() * 100)
];
break;
default:
row = [`ID${i}`, `Item ${i}`, ['Active', 'Inactive'][i % 2], new Date().toLocaleDateString()];
}
rows.push(row);
}
return rows;
}
/**
* Generate gauge data
*/
private generateGaugeData(entitySet: string, config: any): any {
const gaugeTemplates: { [key: string]: any } = {
Customers: {
value: 87.5,
min: 0,
max: 100,
unit: '%',
label: 'Customer Satisfaction',
thresholds: [
{ min: 0, max: 50, color: '#f5222d', label: 'Poor' },
{ min: 50, max: 75, color: '#faad14', label: 'Good' },
{ min: 75, max: 100, color: '#52c41a', label: 'Excellent' }
]
},
Orders: {
value: 92.3,
min: 0,
max: 100,
unit: '%',
label: 'Order Fulfillment Rate',
thresholds: [
{ min: 0, max: 70, color: '#f5222d', label: 'Low' },
{ min: 70, max: 90, color: '#faad14', label: 'Medium' },
{ min: 90, max: 100, color: '#52c41a', label: 'High' }
]
}
};
return gaugeTemplates[entitySet] || {
value: Math.random() * 100,
min: 0,
max: 100,
unit: '%',
label: `${entitySet} Performance`
};
}
/**
* Generate timeline data
*/
private generateTimelineData(entitySet: string, query: any): any {
const events = [];
const count = query.top || 10;
for (let i = 1; i <= count; i++) {
const event = {
id: i,
title: `${entitySet} Event ${i}`,
description: `Important event related to ${entitySet.toLowerCase()}`,
date: new Date(2024, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1),
type: ['info', 'success', 'warning', 'error'][i % 4],
icon: ['📊', '✅', '⚠️', '❌'][i % 4]
};
events.push(event);
}
return events.sort((a, b) => b.date.getTime() - a.date.getTime());
}
/**
* Generate generic data
*/
private generateGenericData(entitySet: string, query: any): any {
return {
entitySet,
recordCount: Math.floor(Math.random() * 1000) + 100,
lastUpdated: new Date().toISOString(),
status: 'active'
};
}
/**
* Prepare dashboard data sources
*/
private async prepareDashboardDataSources(widgets: WidgetConfig[], datasources?: any[]): Promise<DataSourceMapping[]> {
const mappings: DataSourceMapping[] = [];
// Create mappings for widget data sources
widgets.forEach(widget => {
if (widget.dataKey) {
mappings.push({
widgetId: widget.id,
query: `${widget.dataKey}`,
refresh: 60 // Default refresh interval
});
}
});
// Add shared data sources
if (datasources) {
datasources.forEach(ds => {
mappings.push({
widgetId: ds.id,
query: ds.query || ds.entitySet,
refresh: ds.cacheTtl || 300
});
});
}
return mappings;
}
/**
* Create widget components for layout
*/
private createWidgetComponents(widgets: WidgetConfig[]): any[] {
return widgets.map(widget => ({
id: widget.id,
type: widget.type,
config: widget.config,
data: widget.config?.data
}));
}
/**
* Enhance dashboard result with SAP-specific functionality
*/
private async enhanceDashboardResult(dashboardResult: UIRenderResult, params: any): Promise<UIRenderResult> {
// Add Chart.js and SAP-specific CSS
const sapCSS = this.generateSAPDashboardCSS(params.theme || 'sap_horizon');
// Add Chart.js and SAP-specific JavaScript
const sapJS = this.generateSAPDashboardJS(params);
return {
...dashboardResult,
css: (dashboardResult.css || '') + sapCSS,
javascript: (dashboardResult.javascript || '') + sapJS
};
}
/**
* Generate SAP-specific CSS for dashboard
*/
private generateSAPDashboardCSS(theme: string): string {
return `
/* Chart.js CDN - Include in head */
@import url('https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.min.css');
/* SAP ${theme} Dashboard Styles */
.sap-dashboard {
background: #f5f6fa;
min-height: 100vh;
padding: 1.5rem;
font-family: '72', -apple-system, BlinkMacSystemFont, sans-serif;
}
.sap-dashboard-header {
background: white;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e4e7ea;
}
.sap-dashboard-title {
font-size: 1.8rem;
font-weight: 600;
color: #0070f2;
margin: 0 0 0.5rem 0;
}
.sap-dashboard-description {
color: #6a6d70;
margin: 0;
font-size: 1rem;
}
.sap-dashboard-filters {
display: flex;
gap: 1rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.sap-dashboard-filter {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.sap-dashboard-filter label {
font-size: 0.875rem;
font-weight: 600;
color: #32363a;
}
.sap-dashboard-filter select,
.sap-dashboard-filter input {
padding: 0.5rem;
border: 1px solid #d5d9dc;
border-radius: 4px;
font-size: 0.875rem;
min-width: 150px;
}
.sap-dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.sap-dashboard-widget {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e4e7ea;
transition: transform 0.2s ease, box-shadow 0.2s ease;
position: relative;
overflow: hidden;
}
.sap-dashboard-widget:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.sap-widget-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid #f0f2f5;
}
.sap-widget-title {
font-size: 1.1rem;
font-weight: 600;
color: #32363a;
margin: 0;
}
.sap-widget-actions {
display: flex;
gap: 0.25rem;
}
.sap-widget-action {
background: none;
border: none;
color: #6a6d70;
cursor: pointer;
padding: 0.25rem;
border-radius: 4px;
transition: color 0.2s ease, background 0.2s ease;
}
.sap-widget-action:hover {
color: #0070f2;
background: #f0f8ff;
}
/* KPI Card Styles */
.sap-kpi-card {
text-align: center;
}
.sap-kpi-value {
font-size: 2.5rem;
font-weight: 700;
color: #0070f2;
margin: 0.5rem 0;
line-height: 1;
}
.sap-kpi-label {
font-size: 0.875rem;
color: #6a6d70;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.sap-kpi-trend {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.75rem;
border-radius: 16px;
font-size: 0.75rem;
font-weight: 600;
margin-top: 0.5rem;
}
.sap-kpi-trend.positive {
background: #e8f5e8;
color: #2e7d32;
}
.sap-kpi-trend.negative {
background: #ffebee;
color: #c62828;
}
.sap-kpi-trend.warning {
background: #fff8e1;
color: #f57c00;
}
/* Chart Container */
.sap-chart-container {
position: relative;
height: 300px;
margin: 1rem 0;
}
.sap-chart-container canvas {
max-height: 100%;
}
/* Table Styles */
.sap-widget-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
.sap-widget-table th {
background: #f7f8fa;
border-bottom: 1px solid #e4e7ea;
padding: 0.5rem;
text-align: left;
font-weight: 600;
color: #32363a;
font-size: 0.75rem;
text-transform: uppercase;
}
.sap-widget-table td {
padding: 0.5rem;
border-bottom: 1px solid #f0f2f5;
color: #32363a;
}
.sap-widget-table tr:last-child td {
border-bottom: none;
}
/* Gauge Styles */
.sap-gauge-container {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.sap-gauge-value {
font-size: 2rem;
font-weight: 700;
color: #0070f2;
margin: 1rem 0 0.5rem 0;
}
.sap-gauge-label {
font-size: 0.875rem;
color: #6a6d70;
}
/* Timeline Styles */
.sap-timeline {
max-height: 400px;
overflow-y: auto;
}
.sap-timeline-item {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 0.75rem 0;
border-bottom: 1px solid #f0f2f5;
}
.sap-timeline-item:last-child {
border-bottom: none;
}
.sap-timeline-icon {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: #e3f2fd;
color: #0070f2;
font-size: 1rem;
flex-shrink: 0;
}
.sap-timeline-content {
flex: 1;
}
.sap-timeline-title {
font-weight: 600;
color: #32363a;
margin: 0 0 0.25rem 0;
font-size: 0.875rem;
}
.sap-timeline-description {
color: #6a6d70;
font-size: 0.75rem;
margin: 0 0 0.25rem 0;
}
.sap-timeline-date {
color: #6a6d70;
font-size: 0.75rem;
font-family: 'Courier New', monospace;
}
/* Dashboard Controls */
.sap-dashboard-controls {
position: fixed;
top: 1rem;
right: 1rem;
display: flex;
gap: 0.5rem;
z-index: 1000;
}
.sap-control-button {
background: white;
border: 1px solid #d5d9dc;
border-radius: 4px;
padding: 0.5rem;
cursor: pointer;
color: #32363a;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.sap-control-button:hover {
background: #f7f8fa;
border-color: #0070f2;
color: #0070f2;
}
/* Responsive Design */
@media (max-width: 768px) {
.sap-dashboard {
padding: 1rem;
}
.sap-dashboard-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.sap-dashboard-filters {
flex-direction: column;
}
.sap-dashboard-filter select,
.sap-dashboard-filter input {
min-width: auto;
width: 100%;
}
.sap-kpi-value {
font-size: 2rem;
}
.sap-chart-container {
height: 250px;
}
}
`;
}
/**
* Generate SAP-specific JavaScript for dashboard
*/
private generateSAPDashboardJS(params: any): string {
return `
// Chart.js CDN - Include before this script
// <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.min.js"></script>
// SAP Dashboard Manager
class SAPDashboardManager {
constructor(config) {
this.config = config;
this.widgets = new Map();
this.charts = new Map();
this.refreshInterval = config.refreshInterval || 60;
this.filters = new Map();
this.init();
}
init() {
this.setupDashboard();
this.initializeWidgets();
this.setupRefreshTimer();
this.setupEventHandlers();
this.logger.debug('SAP Dashboard Manager initialized:', this.config.title);
}
setupDashboard() {
// Create dashboard structure
const dashboard = document.createElement('div');
dashboard.className = 'sap-dashboard';
dashboard.innerHTML = this.generateDashboardHTML();
document.body.appendChild(dashboard);
}
generateDashboardHTML() {
return \`
<div class="sap-dashboard-header">
<h1 class="sap-dashboard-title">\${this.config.title}</h1>
<p class="sap-dashboard-description">\${this.config.description || ''}</p>
\${this.generateFiltersHTML()}
</div>
<div class="sap-dashboard-grid">
\${this.config.widgets.map(widget => this.generateWidgetHTML(widget)).join('')}
</div>
<div class="sap-dashboard-controls">
<button class="sap-control-button" onclick="window.sapDashboard.refreshAll()" title="Refresh All">
🔄
</button>
<button class="sap-control-button" onclick="window.sapDashboard.exportDashboard('pdf')" title="Export PDF">
📄
</button>
<button class="sap-control-button" onclick="window.sapDashboard.toggleFullscreen()" title="Fullscreen">
🔳
</button>
</div>
\`;
}
generateFiltersHTML() {
if (!this.config.filters || this.config.filters.length === 0) return '';
return \`
<div class="sap-dashboard-filters">
\${this.config.filters.map(filter => \`
<div class="sap-dashboard-filter">
<label>\${filter.label}</label>
\${this.generateFilterInput(filter)}
</div>
\`).join('')}
</div>
\`;
}
generateFilterInput(filter) {
switch (filter.type) {
case 'select':
return \`
<select onchange="window.sapDashboard.applyFilter('\${filter.field}', this.value)">
<option value="">All</option>
\${filter.options.map(opt => \`<option value="\${opt.value}">\${opt.label}</option>\`).join('')}
</select>
\`;
case 'daterange':
return \`<input type="date" onchange="window.sapDashboard.applyFilter('\${filter.field}', this.value)">\`;
case 'number':
return \`<input type="number" placeholder="\${filter.label}" onchange="window.sapDashboard.applyFilter('\${filter.field}', this.value)">\`;
default:
return \`<input type="text" placeholder="\${filter.label}" onchange="window.sapDashboard.applyFilter('\${filter.field}', this.value)">\`;
}
}
generateWidgetHTML(widget) {
return \`
<div class="sap-dashboard-widget" id="widget-\${widget.id}">
<div class="sap-widget-header">
<h3 class="sap-widget-title">\${widget.title}</h3>
<div class="sap-widget-actions">
<button class="sap-widget-action" onclick="window.sapDashboard.refreshWidget('\${widget.id}')" title="Refresh">
🔄
</button>
<button class="sap-widget-action" onclick="window.sapDashboard.exportWidget('\${widget.id}')" title="Export">
📤
</button>
</div>
</div>
<div class="sap-widget-content" id="widget-content-\${widget.id}">
\${this.generateWidgetContent(widget)}
</div>
</div>
\`;
}
generateWidgetContent(widget) {
switch (widget.type) {
case 'kpi-card':
return this.generateKPICardHTML(widget);
case 'chart':
return \`<div class="sap-chart-container"><canvas id="chart-\${widget.id}"></canvas></div>\`;
case 'table':
return this.generateTableHTML(widget);
case 'gauge':
return this.generateGaugeHTML(widget);
case 'timeline':
return this.generateTimelineHTML(widget);
default:
return \`<div>Widget type '\${widget.type}' not implemented</div>\`;
}
}
generateKPICardHTML(widget) {
const data = widget.config.data;
if (!data) return '<div>No data available</div>';
return \`
<div class="sap-kpi-card">
<div class="sap-kpi-label">\${data.label || widget.title}</div>
<div class="sap-kpi-value">\${data.value}\${data.unit || ''}</div>
\${data.trend ? \`<div class="sap-kpi-trend \${data.status || 'positive'}">
\${data.status === 'positive' ? '↗️' : data.status === 'negative' ? '↘️' : '➡️'} \${data.trend}
</div>\` : ''}
</div>
\`;
}
generateTableHTML(widget) {
const data = widget.config.data;
if (!data || !data.headers || !data.rows) return '<div>No data available</div>';
return \`
<table class="sap-widget-table">
<thead>
<tr>
\${data.headers.map(header => \`<th>\${header}</th>\`).join('')}
</tr>
</thead>
<tbody>
\${data.rows.map(row => \`
<tr>
\${row.map(cell => \`<td>\${cell}</td>\`).join('')}
</tr>
\`).join('')}
</tbody>
</table>
\`;
}
generateGaugeHTML(widget) {
const data = widget.config.data;
if (!data) return '<div>No data available</div>';
return \`
<div class="sap-gauge-container">
<svg width="200" height="120" class="sap-gauge-svg">
<path d="M 20 100 A 80 80 0 0 1 180 100" stroke="#e4e7ea" stroke-width="10" fill="none"/>
<path d="M 20 100 A 80 80 0 0 1 \${20 + (160 * data.value / 100)} 100" stroke="#0070f2" stroke-width="10" fill="none"/>
</svg>
<div class="sap-gauge-value">\${data.value}\${data.unit || ''}</div>
<div class="sap-gauge-label">\${data.label || widget.title}</div>
</div>
\`;
}
generateTimelineHTML(widget) {
const data = widget.config.data;
if (!data || !Array.isArray(data)) return '<div>No data available</div>';
return \`
<div class="sap-timeline">
\${data.map(event => \`
<div class="sap-timeline-item">
<div class="sap-timeline-icon">\${event.icon || '📅'}</div>
<div class="sap-timeline-content">
<div class="sap-timeline-title">\${event.title}</div>
<div class="sap-timeline-description">\${event.description}</div>
<div class="sap-timeline-date">\${new Date(event.date).toLocaleDateString()}</div>
</div>
</div>
\`).join('')}
</div>
\`;
}
initializeWidgets() {
this.config.widgets.forEach(widget => {
this.widgets.set(widget.id, widget);
if (widget.type === 'chart') {
this.initializeChart(widget);
}
});
}
initializeChart(widget) {
const canvas = document.getElementById(\`chart-\${widget.id}\`);
if (!canvas || !widget.config.data) return;
const ctx = canvas.getContext('2d');
const chart = new Chart(ctx, widget.config.data);
this.charts.set(widget.id, chart);
}
setupRefreshTimer() {
if (this.refreshInterval > 0) {
setInterval(() => {
this.refreshAll();
}, this.refreshInterval * 1000);
}
}
setupEventHandlers() {
// Add keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case 'r':
e.preventDefault();
this.refreshAll();
break;
case 'f':
e.preventDefault();
this.toggleFullscreen();
break;
}
}
});
}
refreshAll() {
this.logger.debug('Refreshing all dashboard widgets...');
this.widgets.forEach((widget, id) => {
this.refreshWidget(id);
});
}
refreshWidget(widgetId) {
this.logger.debug('Refreshing widget', { widgetId });
// In a real implementation, this would fetch fresh data
const widget = this.widgets.get(widgetId);
if (widget && widget.type === 'chart') {
const chart = this.charts.get(widgetId);
if (chart) {
// Update chart data
chart.data.datasets[0].data = chart.data.datasets[0].data.map(() =>
Math.floor(Math.random() * 800) + 200
);
chart.update();
}
}
}
applyFilter(field, value) {
this.filters.set(field, value);
this.logger.debug('Filter applied', { field, value });
// In a real implementation, this would filter all widgets
this.refreshAll();
}
exportDashboard(format = 'pdf') {
this.logger.debug('Exporting dashboard', { format });
// In a real implementation, this would generate exports
alert(\`Dashboard export (\${format}) functionality would be implemented here\`);
}
exportWidget(widgetId) {
this.logger.debug('Exporting widget', { widgetId });
// In a real implementation, this would export the specific widget
}
toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
}
}
// Initialize dashboard when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
const dashboardConfig = ${JSON.stringify(params, null, 2)};
window.sapDashboard = new SAPDashboardManager(dashboardConfig);
});
`;
}
/**
* Create dashboard response object
*/
private createDashboardResponse(dashboardResult: UIRenderResult, params: any, dataSources: DataSourceMapping[]): any {
return {
dashboardId: `sap-dashboard-${Date.now()}`,
title: params.title,
description: params.description,
layout: params.layout,
widgets: params.widgets,
dataSources: dataSources,
refreshInterval: params.refreshInterval,
theme: params.theme,
filters: params.filters,
exportOptions: params.exportOptions,
html: dashboardResult.html,
css: dashboardResult.css,
javascript: dashboardResult.javascript,
metadata: {
generated: new Date().toISOString(),
version: '1.0.0',
toolName: 'ui-dashboard-composer',
widgetCount: params.widgets.length,
dependencies: ['Chart.js 4.4.0']
}
};
}
/**
* Check UI access permissions
*/
private async checkUIAccess(requiredScope: string): Promise<{ hasAccess: boolean; reason?: string }> {
// In a real implementation, this would check the current user's JWT token
// For now, we'll return true (access granted)
return { hasAccess: true };
}
}