Skip to main content
Glama

SAP OData to MCP Server

by Raistlin82
ui-dashboard-composer-tool.ts51.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 }; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Raistlin82/btp-sap-odata-to-mcp-server-optimized'

If you have feedback or need assistance with the MCP directory API, please join our Discord server