Building a custom dashboard creation interface with compose SDK & Sisense rest APIs
Overview
This use case demonstrates how to build a custom, user-friendly dashboard creation interface that leverages Sisense's powerful analytics engine while providing a tailored user experience. By combining Sisense's robust backend capabilities with a modern React frontend, organizations can create workflows that align perfectly with their specific needs and branding requirements.
Value Proposition
Why Build a Custom Interface?
🎨 Tailored Designer UX: Create interfaces that match your organization's design language and workflows, rather than being constrained by out-of-the-box UI limitations.
👥 Improved User Experience: Design streamlined workflows that reduce complexity for end users while maintaining full analytical capabilities.
🔒 Maintain Enterprise Controls: Leverage Sisense's built-in role-based access controls, security, and governance features without modification.
⚡ Faster Time-to-Insight: Reduce the steps required for users to create meaningful dashboards and visualizations.
🏢 Brand Consistency: Ensure dashboard creation tools match your organization's visual identity and user experience standards.
Technical Stack
Frontend Libraries
{ "@sisense/sdk-ui": "^1.x.x", "@mui/icons-material": "^5.x.x", "@mui/material": "^5.x.x", "react": "^18.x.x", "typescript": "^4.x.x" }
Backend Libraries
{ "express": "^4.18.2", "cors": "^2.8.5", "dotenv": "^16.3.1" }
Implementation Guide
1. Backend API Service Setup
Create a Node.js/Express service to proxy Sisense API calls and handle authentication:
// server.js const express = require('express'); const cors = require('cors'); require('dotenv').config(); const app = express(); app.use(cors()); app.use(express.json()); // Sisense Configuration const SISENSE_CONFIG = { baseUrl: process.env.SISENSE_BASE_URL, apiToken: process.env.SISENSE_API_TOKEN, dataSource: { title: 'Sample ECommerce', id: 'aLOCALHOST_aSAMPLEIAAaECOMMERCE', // ... other datasource config } }; // Helper function for Sisense API requests async function sisenseApiRequest(endpoint, method = 'GET', body = null) { const url = `${SISENSE_CONFIG.baseUrl}/api/v1${endpoint}`; const options = { method, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${SISENSE_CONFIG.apiToken}`, }, }; if (body) options.body = JSON.stringify(body); const response = await fetch(url, options); if (!response.ok) { const errorText = await response.text(); throw new Error(`Sisense API Error: ${response.status} ${errorText}`); } return await response.json(); }
2. Key API Endpoints
Dashboard Management
// List all dashboards app.get('/api/dashboards', async (req, res) => { try { const dashboards = await sisenseApiRequest('/dashboards'); res.json({ success: true, dashboards: dashboards.map(d => ({ id: d.oid || d._id, title: d.title, created: d.created, lastUpdated: d.lastUpdated })) }); } catch (error) { res.status(500).json({ error: 'Failed to list dashboards', details: error.message }); } }); // Create new dashboard app.post('/api/dashboards', async (req, res) => { try { const { title, description } = req.body; const payload = { title: title.trim(), desc: description || 'Dashboard created from custom UI', datasource: SISENSE_CONFIG.dataSource, layout: { type: 'columnar', columns: [{ width: 100, cells: [] }] } }; const dashboard = await sisenseApiRequest('/dashboards', 'POST', payload); res.json({ success: true, dashboard: { id: dashboard.oid || dashboard._id, title: dashboard.title, created: dashboard.created, } }); } catch (error) { res.status(500).json({ error: 'Failed to create dashboard', details: error.message }); } });
Widget Creation
// Add widgets to dashboard app.post('/api/dashboards/:dashboardId/widgets', async (req, res) => { try { const { dashboardId } = req.params; const { widgets } = req.body; const results = []; for (let i = 0; i < widgets.length; i++) { const widget = widgets[i]; if (widget.widgetType !== 'chart') continue; const payload = processWidgetForSisense(widget, i); const savedWidget = await sisenseApiRequest( `/dashboards/${dashboardId}/widgets`, 'POST', payload ); results.push({ id: savedWidget.oid || savedWidget._id, title: savedWidget.title, type: savedWidget.type, }); } res.json({ success: true, saved: results.length, widgets: results }); } catch (error) { res.status(500).json({ error: 'Failed to create widgets', details: error.message }); } });
3. Widget Data Processing
Transform Sisense SDK objects to API-compatible format:
function processWidgetForSisense(widget, index = 0) { const chartWidget = widget; // Chart type mapping const chartTypeMapping = { 'column': 'bar', 'bar': 'bar', 'line': 'line', 'pie': 'pie' }; const sisenseChartType = chartTypeMapping[chartWidget.chartType] || 'bar'; // Extract and process field data const category = chartWidget.dataOptions?.category?.[0]; const measure = chartWidget.dataOptions?.value?.[0]; const categoryName = category?._name || category?.name || 'Unknown Category'; const measureName = measure?._name || measure?.name || 'Unknown Measure'; // Format for Sisense API bracket notation const categoryDim = `[Commerce.${categoryName.replace(/\s+/g, ' ')}]`; const measureDim = `[Commerce.${measureName.replace(/\s+/g, ' ').replace('sum ', '')}]`; return { title: chartWidget.title || `Chart Widget ${index + 1}`, desc: 'Widget created from custom UI', datasource: SISENSE_CONFIG.dataSource, type: `chart/${sisenseChartType}`, subtype: sisenseChartType, metadata: { ignore: { ids: [], dimensions: [], all: false }, panels: [ { name: 'categories', items: category ? [{ jaql: { dim: categoryDim, datatype: 'text', title: categoryName, }, field: { id: `[Commerce.${categoryName.replace(/\s+/g, '')}]_1`, index: 0, }, format: {}, }] : [], }, { name: 'values', items: measure ? [{ jaql: { dim: measureDim, datatype: 'numeric', title: measureName, agg: 'sum', }, field: { id: `[Commerce.${measureName.replace(/\s+/g, '').replace('sum', '')}]_1`, index: 0, }, format: {}, }] : [], }, { name: 'break by', items: [] }, ], }, style: { widgetDesign: { widgetSpacing: 'none', widgetBackgroundColor: '#FFFFFF', // ... additional styling }, height: 280, }, }; }
4. Frontend Implementation
Dashboard Selection Interface
// React component for dashboard selection const DashboardSelector = () => { const [existingDashboards, setExistingDashboards] = useState<DashboardSummary[]>([]); const [isLoading, setIsLoading] = useState(false); const fetchDashboards = async () => { setIsLoading(true); try { const response = await fetch(`${BACKEND_URL}/api/dashboards`); const data = await response.json(); setExistingDashboards(data.dashboards || []); } catch (error) { console.error('Failed to fetch dashboards:', error); } finally { setIsLoading(false); } }; useEffect(() => { fetchDashboards(); }, []); return ( <div> {existingDashboards.map((dashboard) => ( <DashboardCard key={dashboard.id} dashboard={dashboard} onSelect={(id) => selectDashboard(id)} /> ))} </div> ); };
Widget Creation Integration
// Integration with Sisense Compose SDK const WidgetBuilder = ({ dashboardId }: { dashboardId: string }) => { const [newWidgets, setNewWidgets] = useState<WidgetProps[]>([]); const { dashboard } = useGetDashboardModel({ dashboardOid: dashboardId, includeFilters: true, includeWidgets: true, }); const saveWidgets = async () => { try { const response = await fetch( `${BACKEND_URL}/api/dashboards/${dashboardId}/widgets`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ widgets: newWidgets }), } ); const result = await response.json(); if (result.success) { setNewWidgets([]); alert(`Saved ${result.saved} widgets successfully!`); } } catch (error) { console.error('Failed to save widgets:', error); } }; return ( <div> {dashboard && ( <Dashboard {...dashboardModelTranslator.toDashboardProps(dashboard)} config={{ filtersPanel: { visible: false } }} /> )} <button onClick={saveWidgets}> Save Widgets ({newWidgets.length}) </button> </div> ); };
Sisense API Endpoints Used
Core Endpoints
- GET /api/v1/dashboards - List all dashboards
- POST /api/v1/dashboards - Create new dashboard
- GET /api/v1/dashboards/{id} - Get specific dashboard
- POST /api/v1/dashboards/{id}/widgets - Add widgets to dashboard
Authentication
All requests require Bearer token authentication: You can set this up with the SSO too - for the example I just hardcoded token.
Authorization: Bearer {your-sisense-api-token}
Environment Setup
Backend Environment Variables
# .env PORT=3001 SISENSE_BASE_URL=https://your-sisense-instance.com SISENSE_API_TOKEN=your_api_token_here
Frontend Environment Variables
# .env REACT_APP_BACKEND_URL=http://localhost:3001
Key Considerations
Licensing Requirements
- Designer License Required: This solution requires Sisense Designer licenses or above for users who will be creating dashboards
- Token Management: Implement proper token rotation and security practicesBenefits Realized
✅ Improved User Adoption: Create tailored workflows that align with your product's UX.
✅ Consistent Branding: Custom UI matches organizational design language
✅ Faster Dashboard Creation: Streamlined workflow reduces clicks and complexity
✅ Maintained Security: Full Sisense RBAC and security features preserved
Conclusion
This approach demonstrates how organizations can leverage Sisense's powerful analytics capabilities while providing users with a custom interface that fits their specific workflow requirements. By maintaining the underlying Sisense security and data governance features while improving the user experience, teams can achieve faster adoption and more efficient dashboard creation processes.
The modular architecture of Compose SDK also allows for future enhancements such as custom widget templates, automated dashboard generation, and integration with existing enterprise applications.