Sisense Community logo
     
    • Community Feedback
    • Chapters
    • Events
    • Forums
      • Help and How To
      • Product Feedback Forum
      • Strategy & Use Cases
    • Blogs
    • KB Docs
      • KB Docs
      • Add-Ons & Plug-Ins
      • APIs
      • Best Practices
      • Blox
      • CDT
      • Cloud Managed Service
      • Data Models
      • Data Sources
      • Embedding Analytics
      • How-Tos & FAQs
      • Onboarding
      • PySisense
      • Security
      • Sisense Administration
      • Sisense Intelligence & AI
      • Troubleshooting
      • Widget & Dashboard Scripts
    • Support
    • Learning
      • Sisense Academy: Free Courses and Certifications
      • Official Developer Documentation
      • Official Product Documentation
      • Official Sisense Youtube Channel
      • Sisense Compose SDK Playground
      • Official Sisense Discord
    • Use Case Gallery
    •      
    Discussions
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
    Discussions
    • TagsChevronRightIcon
    Compose SDK
    • Micael Santana

      Knowledge Base Docs

               
      Micael Santana
      Posted 2 weeks ago
      How to Replace Plugin Widgets in React Compose SDK [Linux]
                               

      This article explains how to replace plugin widgets inside embedded dashboards that use Compose SDK. You’ll learn how the system works internally, registration functions, and how to override the default behavior of the registration. Step 1: Understand the goal When working with embedded dashboards that contain Custom Widget Plugins, for example: Histogram, Tabber etc. The default behavior of CSDK is show an error placeholder: The main idea of Custom Widgets is to replace them, so the CSDK renders it instead of the original widget inside the DashboardById component. Step 2: Custom Component To start you must create the component itself, to do it you should implement CustomWidgetComponent , and fetch widget data using the useExecuteCustomWidgetQuery hook. import { CustomWidgetComponent, useExecuteCustomWidgetQuery } from '@sisense/sdk-ui'; const ResultsTable: CustomWidgetComponent = (props) => { const { data } = useExecuteCustomWidgetQuery(props); if (!data) { return null; } return ( <table style={{ margin: '20px' }}> <thead> <tr> {data.columns.map((column, columnIndex) => ( <th key={columnIndex}>{column.name}</th> ))} </tr> </thead> <tbody> {data.rows.map((row, rowIndex) => ( <tr key={rowIndex}> {row.map((cell, cellIndex) => ( <td key={cellIndex}>{cell.text}</td> ))} </tr> ))} </tbody> </table> ); }; What it does: The component itself it's just a component from React, so you implement: charts, simple tables etc. The key is the useExecuteCustomWidgetQuery that executes the widget query and returns structured data so your component can render however you want. Note: If you prefer working with raw query results instead of formatted data, you should use extractDimensionsAndMeasures together with useExecuteQuery . Step 3: The Data The CustomWidgetComponent receives properties similar to those by plugins internally, like: metadata, filters, configuration. This means: Some logic from plugin widgets can often be reused. Data remains compatible with Fusion. Migration from plugin widgets → Compose SDK is simplified Compose SDK mainly handles rendering, while configuration and data still come from the Sisense. Step 4: Register the custom widget After creating your component, you must register it before rendering the dashboard. Registration tells the SDK which component should be used when it encounters a Custom Widget Type. import {DashboardById,useCustomWidgets }from'@sisense/sdk-ui'; functionApp() { const { registerCustomWidget }=useCustomWidgets(); registerCustomWidget('histogramwidget',ResultsTable); return<DashboardByIddashboardOid={'66f4d4dd384428002ae0a21d'}/>; } Important Registration must happen before the dashboard renders. Once registered, the widget is stored inside the main Compose SDK context. Any dashboard within that context will automatically use your custom widget. Step 5: Understand more about how it works internally Internally, the SDK keeps registered widgets inside a registry managed by the CSDK Context: So the rendering flow will be: Dashboard loads widget metadata. CSDK checks if that widget type exists in the registry: If registered → renders your custom component If not → renders the error placeholder The registry uses a key-value structure for fast lookup and to avoid repeated registrations. Step 6: Why widgets are designed to register only once Registration happens once per context lifecycle by design. This provides several benefits: Performance: prevents repeated registrations on re-renders Predictability: ensures only one implementation per widget type Stability: avoids conflicts between components trying to register the same widget Memory safety: prevents the registry from growing uncontrollably Since registrations are stored in a Context map, calling registerCustomWidget multiple times is unnecessary and discouraged. Step 7: How to unregister it? If you need to unregister a component, recently we added a new method named unregisterCustomWidget , to the CSDK Custom Widgets, that you can use to do it: Note: this was added in v2.26.0 , of Compose SDK. import { useCustomWidgets } from '@sisense/sdk-ui'; export const FunnelWidget = () => { const { registerCustomWidget, unregisterCustomWidget } = useCustomWidgets(); const [type, setType] = useState<'funnel' | 'treemap'>('funnel'); const widget = type === 'funnel' ? CustomFunnel : CustomTreemap; useEffect(() => { registerCustomWidget('histogramwidget', widget); return () => unregisterCustomWidget('histogramwidget'); }, [registerCustomWidget, unregisterCustomWidget, widget]); return ( <div> <Select value={type} onValueChange={(v) => setType(v as 'funnel' | 'treemap')} > <SelectTrigger className="w-[180px] my-3"> <SelectValue placeholder="Chart Type" /> </SelectTrigger> <SelectContent> <SelectGroup> <SelectItem value="funnel">Funnel</SelectItem> <SelectItem value="treemap">Treemap</SelectItem> </SelectGroup> </SelectContent> </Select> <DashboardById key={`dashboard-${type}`} dashboardOid={'68dae1429e8028a69c6a41ee'} /> </div> ); }; Using it you don't need to re-render the Sisense Main Context to change the register type, but you still need to re-render the Dashboard Component. Best practice recommendation Register custom widgets once during application initialization whenever possible. Only force a context remount if you specifically need dynamic registration behavior. Conclusion:  Custom Widgets in Compose SDK provide a powerful way to replace plugin widgets while still using the original widget data. Understanding that registration is stored in a context-level registry explains: why widgets are registered once, why repeated registration isn’t needed and why remounting the context is sometimes required for advanced scenarios. References/Related Content  Compose SDK documentation: DashboardById Component . Compose SDK documentation: Custom Widgets . Compose SDK documentation: Sinsense Context Provider . Mentioned Plugins: Tabber , Histogram . Disclaimer: This post outlines a potential custom workaround for a specific use case or provides instructions regarding a specific task. The solution may not work in all scenarios or Sisense versions, so we strongly recommend testing it in your environment before deployment. If you need further assistance with this, please let us know.

                                             
      0
               
    • Blog banner
      • Widget & Dashboard ScriptsChevronRightIcon

      Programmatically Formatting Bar Chart Widget Value Labels in Sisense

                                                                                               

      Programmatically Formatting Bar Chart Widget Value Labels in Sisense This article outlines ways to programmatically format Sisense Bar Chart Widget Value labels via widget scripts , covering methods to prevent label overlap and apply consistent styling across all labels. Custom Styling for Data Labels The script below enables the formatting of Chart Widget Value labels by setting a custom background color, padding, and border-radius. Ensure the default data label UI option is disabled. Other CSS and Highcharts settings can be added as needed.         widget.on('render', function (se, ev) { ev.widget.queryResult.plotOptions.bar.dataLabels = { backgroundColor: '#f5d142', color: 'white', padding: 5, borderRadius: 5, enabled: true } })       Preventing Label Overlap The script below manually adjusts value label positioning to prevent overlap in densely populated bar chart widgets. The exact formulas for label positioning can be changed as needed.         widget.on('domready', function (se, ev) { var barWidth = $('.highcharts-series-group .highcharts-series rect', element).width(); $('.highcharts-data-labels .highcharts-label', element).each(function () { var labelWidth = $(this).find('rect').width(); var labelHeight = $(this).find('rect').height(); $(this).find('rect').attr('x', ($(this).find('rect').attr('x') + 2)); $(this).find('rect').attr('height', barWidth); $(this).find('rect').attr('y', ((labelHeight - barWidth) / 2)); }) })         Dynamically Increase Space for Labels If bar value labels overlap with the chart bars, you can dynamically adjust the maximum value on the y-axis to create additional space. A different formula, or a hard-coded value, can also be used as the y-axis maximum value.         widget.on('processresult', function (se, ev) { var maxValue = 0; var increasePercent = 0.2; ev.result.series.forEach(function (series) { series.data.forEach(function (dataItem) { if (dataItem.y > maxValue) maxValue = dataItem.y; }) ev.result.yAxis[0].max = maxValue + (increasePercent * maxValue); }) })       Conclusion These scripts enable customizing dynamically formatted and well-positioned data labels in your Sisense charts, enhancing readability and aesthetics beyond the default Sisense data bar data labels in bar chart widgets. For further discussion of these types of scripts, see the  Dynamically Formatted Data Labels article Example Of Custom Labels Added via Scripting   Y-Axis Maximum Set To a Very Large Value Check out this related content:  Academy Documentation

      Jeremy Friedel
      Jeremy FriedelPosted 1 year ago • Last reply 1 month ago
      1
               
    • kundankumar

      Help and How-To

               
      kundankumar
      Posted 8 months ago • Last reply 6 months ago
      Widget Error
                               

      I’m working on integrating a few Sisense dashboards into my project, but I’m running into this error. import React, { useMemo, useEffect, useState } from 'react'; import { useApi, configApiRef } from '@backstage/core-plugin-api'; import { SisenseContextProvider, WidgetById } from '@sisense/sdk-ui'; import { filterFactory } from '@sisense/sdk-data'; import SisenseDashboard from './SisenseDashboard'; import solutionSisenseOrg from '../../solution-sisense-org-mapping.json';   const SISENSE_URL = ''; const SISENSE_TOKEN = '';   type SisenseOrgMapping = Record<string, Record<string, any>>;   type SisenseChartProps = {   dashboardOid: string;   tab: string;   widgetLists?: [];   isWidgetRequired?: boolean; };   const centerStyle = {   minHeight: '40vh',   display: 'flex',   flexDirection: 'column',   alignItems: 'center',   justifyContent: 'center',   fontSize: '1.1rem',   background: 'none', };   export const SisenseChart: React.FC<SisenseChartProps> = ({   dashboardOid,   tab,   widgetLists = [],   isWidgetRequired = true, }) => {   const config = useApi(configApiRef);   const { sisenseUrl, sisenseToken } = useMemo(     () => ({       sisenseUrl: config.getOptionalString('sisense.url') ?? SISENSE_URL,       sisenseToken: config.getOptionalString('sisense.token') ?? SISENSE_TOKEN,     }),     [config],   );     // Extract current org from URL   const currentUrl = window.location.href;   const solutionOrg = useMemo(() => {     const match = currentUrl.match(/\/system\/([^/]+)/);     return match?.[1] ?? '';   }, [currentUrl]);     // Map to Sisense org   const mappedSisenseOrg = useMemo(() => {     if (!solutionSisenseOrg || typeof solutionSisenseOrg !== 'object')       return undefined;     return (solutionSisenseOrg as SisenseOrgMapping)[solutionOrg];   }, [solutionOrg]);     // State for dashboard filters   const [filters, setFilters] = useState<any[]>([]);   const [isLoading, setLoading] = useState(true);   const [error, setError] = useState<string | null>(null);     // Reset filters/loading when dashboardOid or tab changes   useEffect(() => {     setFilters([]);     setLoading(true);     setError(null);   }, [dashboardOid, tab]);     // Fetch dashboard filters   useEffect(() => {     if (!dashboardOid || !sisenseUrl || !sisenseToken) return;       const fetchDashboard = async () => {       try {         const response = await fetch(           `${sisenseUrl}/api/v1/dashboards/${dashboardOid}`,           { headers: { Authorization: `Bearer ${sisenseToken}` } },         );         if (!response.ok) throw new Error('Failed to fetch dashboard');         const data = await response.json();         setFilters(data.filters || []);         setLoading(false);       } catch (err: any) {         setError('Failed to load dashboard filters.');         setLoading(false);       }     };     fetchDashboard();   }, [dashboardOid, sisenseUrl, sisenseToken]);     console.log('Fetched filters:', filters);   // Memoize jaqlFilters   const jaqlFilters = useMemo(() => {     // Extract filters from levels     const levelFilters =       filters.flatMap(         (filter: any) =>           filter.levels?.filter(             (level: any) =>               level.table === 'projects' ||               level.table === 'products' ||               // (level.table === 'Business_Unit_Sols' && level.column === 'solution') ||               (level.table === 'rca_all_issues' &&                 (level.column === 'solutions' ||                   level.column === 'incident_date' ||                   level.column === 'incident_date (Calendar)')) ||               (level.table === 'defect_metrics' && level.column === 'project_name') ||               (level.table === 'quality_scorecard' && level.column === 'filter_solution'),           ) || [],       ) || [];       // Extract root jaql filters     const rootJaqlFilters =       filters         .filter((filter: any) => filter.jaql)         .map((filter: any) => filter.jaql)         .filter(           (jaql: any) =>             (jaql.table === 'rca_all_issues' &&               (jaql.column === 'solutions' ||                 jaql.column === 'incident_date' ||                 jaql.column === 'incident_date (Calendar)')) ||             (jaql.table === 'Business_Unit_Sols' && jaql.column === 'solution') ||             (jaql.table === 'projects') ||             (jaql.table === 'products')         );       return [...levelFilters, ...rootJaqlFilters];   }, [filters]);   // Memoize jaql, attribute, filterquery (always called, never conditionally)   let hasValidFilter = null;   let members = null;   if (mappedSisenseOrg && mappedSisenseOrg[tab]) {     if (typeof mappedSisenseOrg[tab] === 'string') {       members = [mappedSisenseOrg[tab]];       hasValidFilter = Boolean(         jaqlFilters.length &&         jaqlFilters[0]?.dim &&         mappedSisenseOrg &&         mappedSisenseOrg[tab],       );     } else if (typeof mappedSisenseOrg[tab] === 'object') {       hasValidFilter = Boolean(         jaqlFilters.length &&         jaqlFilters[0]?.dim &&         mappedSisenseOrg &&         mappedSisenseOrg[tab] &&         mappedSisenseOrg[tab][dashboardOid],       );       members = [mappedSisenseOrg[tab][dashboardOid]];     }   }     const jaql = useMemo(     () =>       hasValidFilter         ? {           dim: jaqlFilters[0]?.dim,           datatype: jaqlFilters[0]?.datatype || 'text',           isDashboardFilter: jaqlFilters[0]?.isDashboardFilter || false,           title: jaqlFilters[0]?.title,           collapsed: jaqlFilters[0]?.collapsed || false,           filter: {             explicit: true,             multiSelection: true,             members: members ? members : [],           },         }         : null,     [members, hasValidFilter, jaqlFilters],   );     const attribute = useMemo(     () =>       hasValidFilter         ? {           jaql: () => ({             jaql: {               dim: jaqlFilters[0]?.dim,               datatype: jaqlFilters[0]?.datatype || 'text',               isDashboardFilter: jaqlFilters[0]?.isDashboardFilter || false,               title: jaqlFilters[0]?.title,               collapsed: jaqlFilters[0]?.collapsed || false,               filter: {                 explicit: true,                 multiSelection: true,                 members: members ? members : [],               },             },           }),           serialize: () => ({             dim: jaqlFilters[0]?.dim,             datatype: jaqlFilters[0]?.datatype || 'text',             title: jaqlFilters[0]?.title,           }),           id: jaqlFilters[0]?.dim,         }         : null,     [members, hasValidFilter, jaqlFilters],   );     const filterquery: any = useMemo(() => {     if (!hasValidFilter || !attribute || !jaql) return null;     return filterFactory.customFilter(attribute as any, jaql as any, {       disabled: false,     });   }, [hasValidFilter, attribute, jaql]);     if (!sisenseUrl || !sisenseToken) return null;   if (!solutionOrg)     return (       <div style={centerStyle as React.CSSProperties}>         Could not determine organization from URL.       </div>     );   if (!mappedSisenseOrg)     return (       <div style={centerStyle as React.CSSProperties}>         No Sisense organization mapping found for "{solutionOrg}".       </div>     );   if (error)     return <div style={centerStyle as React.CSSProperties}>{error}</div>;   if (isLoading)     return (       <div style={centerStyle as React.CSSProperties}>         <div className="sisense-loader" style={{ marginBottom: 12 }}>           {' '}           Loading...{' '}         </div>       </div>     );   if (!hasValidFilter || !filterquery) {     return (       <div style={centerStyle as React.CSSProperties}>         No valid filter found for this {tab} dashboard.       </div>     );   }     const renderWidgets = () => (     <div>       {widgetLists.map((widgetOid, idx) => (         <WidgetById           key={widgetOid || idx} // Ensure key is never null           widgetOid={widgetOid}           dashboardOid={dashboardOid}           includeDashboardFilters={false}           filters={[filterquery]}         />       ))}     </div>   );     let contentToShow: React.ReactNode = null;     if (isWidgetRequired && widgetLists.length > 0) {     contentToShow = renderWidgets();   } else if (!isWidgetRequired && widgetLists.length === 0) {     contentToShow = null;   } else {     contentToShow = (       <SisenseDashboard filterquery={filterquery} dashboardOid={dashboardOid} />     );   }     return (     <SisenseContextProvider url={sisenseUrl} token={sisenseToken}>       {contentToShow}     </SisenseContextProvider>   ); }; --------------------- import React from 'react'; import {   Dashboard,   dashboardModelTranslator,   useGetDashboardModel,   useCustomWidgets, } from '@sisense/sdk-ui'; import CustomHistogramWidget from './CustomHistogramWidget';   interface SisenseDashboardProps {   filterquery: any[];   dashboardOid: string; }   const SisenseDashboard: React.FC<SisenseDashboardProps> = ({   filterquery,   dashboardOid, }) => {   const { dashboard } = useGetDashboardModel({     dashboardOid,     includeFilters: false,     includeWidgets: true,   });   const { registerCustomWidget } = useCustomWidgets();     if (!dashboard) return null;     const { title, widgets, layoutOptions, styleOptions, widgetsOptions } =     dashboardModelTranslator.toDashboardProps(dashboard);     registerCustomWidget('histogramwidget', CustomHistogramWidget);     // console.log('Dashboard Props:widgets', widgets);     // Add a non-null key property to each widget using its id     console.log('Dashboard Widgets:', layoutOptions);   const widgetsWithValidId = Array.isArray(widgets)   ? widgets.map((widget: any, idx: number) => {       let id =         typeof widget?.id === 'string' && widget.id.trim()           ? widget.id.trim()           : `widget-${idx}`;       if (!id) {         console.warn('Widget missing id:', widget);         id = `widget-${idx}`;       }       const widgetType =         widget.widgetType || widget.type || widget.kind || widget.category || '';       const widgetProps: any = { ...widget, id, key: id};       if (         widgetType !== 'text' &&         widgetType !== 'TextWidget' &&         widgetType !== 'TEXT'       ) {         widgetProps.filters = filterquery;       } else {         delete widgetProps.filters;       }       return widgetProps;     })   : []; return (   <div style={{ marginBottom: '24px' }}>     <Dashboard       id={dashboardOid}       title={title}       layoutOptions={layoutOptions}       widgets={widgetsWithValidId}       filters={filterquery}       config={{ filtersPanel: { visible: false } }}       styleOptions={styleOptions}       widgetsOptions={widgetsOptions}     />   </div> ); };   export default SisenseDashboard; ERROR: 1.  dimension, [rca_all_issues.incident_date (Calendar)], was not found. rcas_and_related_actions {"dimension":"[rca_all_issues.incident_date (Calendar)]"}  2. Value cannot be null. (Parameter 'key') rcas_and_related_action

                                             
      7
               
    • Blog banner
      • Use Case GalleryChevronRightIcon

      Jira Automation App with Sisense + GenAI

               

      A fully automated analytics-to-action workflow. Sisense triggers threshold-based alerts using Compose SDK queries, then through a POST request directly to Jira via API. The ticket is auto-populated with narrative insights generated using Sisense’s GenAI capabilities. Closes the loop between insight and action - reducing response times, improving accountability, and eliminating manual ticket creation in operational workflows. Check out this public Github repo for detailed steps.

      spuma
      spumaPosted 11 months ago
      0
               
    • Blog banner
      • Use Case GalleryChevronRightIcon

      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.    

      spuma
      spumaPosted 11 months ago
      0
               
    • Blog banner
      • Use Case GalleryChevronRightIcon

      Composable combo chart for flexible self-Service

               

      A single, dynamic charting component built entirely with the Compose SDK. Users can interactively select dimensions, swap measures, and apply filters - turning a single chart into a flexible, self-contained dashboard experience. Empowers end-users with self-service exploration without requiring multiple dashboards, reducing dashboard sprawl and enhancing agility. Check out this public Github repo for specific guidance.

      spuma
      spumaPosted 1 year ago
      0
               
    • Blog banner
      • Use Case GalleryChevronRightIcon

      AOTP view with enhanced UI layer

               

      A dashboard-as-a-component pattern using Sisense native widgets with added custom UI components to enrich the experience. This includes buttons, tooltips, tabs, or other UI enhancements layered on top of standard visualizations. Provides a streamlined user experience that preserves the power of Sisense widgets while enabling enhanced interactions and bespoke design elements. Check out this public Github repo for detailed steps.

      spuma
      spumaPosted 1 year ago
      0
               
    • Blog banner
      • Use Case GalleryChevronRightIcon

      AG grid integration with Sisense data

               

      This integration combines Sisense’s Compose SDK with AG Grid's high-performance data tables in a React framework. Queries are powered directly from Sisense, enabling seamless interaction with native data models while preserving built-in security and row-level permissions. Delivers enterprise-grade tabular analytics with powerful filtering, sorting, and exporting features, while maintaining centralized governance and access control. Check out this public Github repo for detailed steps.

      spuma
      spumaPosted 1 year ago
      0
               
    • Blog banner
      • News & UpdatesChevronRightIcon

      Enhancing web security: A deep dive into single sign-on (SSO) and web access tokens (WAT)

                                               

      Single Sign-On (SSO) and Web Access Tokens (WAT) are two pivotal technologies in the domain of web security and user authentication. SSO simplifies the user experience by enabling access to multiple applications with a single set of login credentials, thereby enhancing both convenience and security. Conversely, WAT plays a crucial role in secure access management by granting temporary tokens that verify user identity and permissions across web applications. Both SSO and WAT are integral to creating a secure and seamless digital environment, each addressing unique facets of user authentication and access control. In the following sections, we will explore the mechanisms, benefits, and implementations of SSO and WAT. Understanding Single Sign-On (SSO) Single Sign-On (SSO) is a centralized authentication process that allows users to log in once and gain access to multiple applications without being prompted to log in again for each one. This technology is particularly beneficial in environments where users need to switch between different systems frequently. By reducing the number of login prompts, SSO not only enhances user convenience but also minimizes the risk of password fatigue and the associated security risks, such as password reuse or weak passwords. Moreover, SSO streamlines the management of user credentials, making it easier for administrators to enforce security policies and track access patterns. How SSO Works Single Sign-On (SSO) operates by establishing a trusted relationship between an identity provider (IdP) and multiple service providers (SPs). The IdP is responsible for authenticating the user's credentials, while the SPs are the various applications or services the user needs to access. This trusted relationship allows the user to authenticate once and gain access to multiple applications seamlessly. Step-by-Step Process: Initial Authentication: When a user attempts to access an application (SP), they are redirected to the identity provider (IdP) if they are not already authenticated. The IdP presents a login page where the user enters their credentials (e.g., username and password). Credential Verification: The IdP verifies the user's credentials. If the credentials are correct, the IdP generates an authentication token or ticket. This token contains information about the user and is securely signed to prevent tampering. Token Exchange: The authentication token is sent back to the user's browser, which then forwards it to the original application (SP) the user tried to access. The application verifies the token's validity by checking the IdP's signature. Access Granted: Once the token is verified, the user is granted access to the application without needing to enter their credentials again. The token can be used for subsequent requests to other SPs, enabling seamless access to multiple applications. Single Logout: SSO systems often include a single logout feature, where logging out from the IdP also logs the user out from all connected applications. This ensures that the user's session is securely terminated across all services. Underlying Technologies: SSO relies on several protocols and technologies to function effectively. Some common protocols include: SAML (Security Assertion Markup Language): A widely used standard for exchanging authentication and authorization data between an IdP and SPs. SAML uses XML-based messages to communicate assertions about user authentication and access rights. OAuth: An open standard for token-based authentication and authorization, commonly used for granting third-party applications limited access to user resources without exposing passwords. OpenID Connect: An authentication layer built on top of OAuth 2.0, allowing clients to verify the identity of users based on the authentication performed by an IdP and to obtain basic profile information. These protocols ensure secure and efficient communication between the IdP and SPs, enabling the seamless and secure exchange of authentication tokens. Security Measures: SSO systems implement several security measures to protect user credentials and authentication tokens: Encryption: Authentication tokens are encrypted to protect sensitive information during transmission. Digital Signatures: Tokens are digitally signed to prevent tampering and ensure their integrity. Multi-Factor Authentication (MFA): SSO can integrate MFA to add an extra layer of security, requiring users to provide additional verification (e.g., a code sent to their phone) beyond just a password. Session Management: SSO systems manage user sessions effectively, ensuring that tokens are valid only for a specific period and are renewed or revoked as needed. By combining these security measures and protocols, SSO systems provide a robust framework for managing user authentication across multiple applications, enhancing both security and user convenience. Pros and Cons of SSO Pros Cons Improved User Experience: Users only need to log in once to access multiple applications, reducing login fatigue. Simplifies the user experience by eliminating the need to remember multiple passwords. Enhanced Security: Centralized authentication allows for the implementation of stronger password policies. Easier to enforce multi-factor authentication (MFA) across all connected applications. Streamlined Administration: Simplifies user account management and access control. Easier to monitor and audit user access and behavior. Facilitates quick revocation of access when needed. Cost Efficiency: Reduces IT support costs related to password resets and account recovery. Saves time for users and administrators, improving overall productivity. Consistent User Policies: Ensures uniform security and access policies across multiple applications. Enhances compliance with regulatory requirements.   Single Point of Failure: If the SSO provider experiences downtime, users lose access to all connected applications. A compromised SSO account can potentially expose multiple applications to security risks. Complex Integration: Integrating SSO with existing applications can be technically challenging. Requires significant initial setup and configuration effort. Dependency on Third-Party Providers: Relying on external SSO providers can introduce risks related to service availability and data privacy. Potential for vendor lock-in if the SSO provider's technology becomes deeply integrated. Security Risks: Centralized authentication can make SSO systems attractive targets for attackers. Requires robust security measures to protect the SSO system itself. Scalability Issues: As the number of applications and users grows, the SSO system may face performance challenges. Ensuring consistent performance and availability can be complex. Useful Materials: Sisense SSO Documentation Sisense Composed SDK Authentication & Security Documentation Exploring Web Access Tokens (WAT) Web Access Tokens (WAT) are digital credentials used to authenticate and authorize users across web applications. Unlike traditional session management mechanisms, WATs provide a more scalable and secure method of handling user sessions. Typically, these tokens are used in stateless authentication systems, where the server does not maintain any session information. Instead, all necessary user information is encoded within the token itself, which the client includes in each request to access protected resources. How Web Access Tokens (WAT) Work Web Access Tokens (WAT), commonly implemented as JSON Web Tokens (JWT), are digital credentials used to authenticate and authorize users across web applications. Unlike traditional session-based authentication, WATs provide a stateless mechanism, meaning the server does not need to maintain session information. Instead, all necessary user information is encoded within the token itself, which the client includes in each request to access protected resources. Step-by-Step Process: User Authentication: When a user logs in to an application, they provide their credentials (e.g., username and password). The application sends these credentials to an authentication server for verification. Token Generation: Upon successful authentication, the authentication server generates a token, typically a JWT. This token contains encoded information about the user, such as their user ID, roles, and permissions. The token is digitally signed using a secret key or public/private key pair to ensure its integrity and authenticity. Token Storage: The generated token is sent back to the client (e.g., the user's browser or mobile app), where it is stored securely, often in local storage or a secure cookie. It's crucial to handle the token securely to prevent unauthorized access, such as through cross-site scripting (XSS) attacks. Token Usage: For subsequent requests to access protected resources, the client includes the token in the request header, typically in the Authorization header as a Bearer token. For example, the header might look like this: Authorization: Bearer <token>. Token Verification: When the server receives a request with a token, it verifies the token's signature to ensure it hasn't been tampered with. The server also checks the token's validity, including its expiration time and any other claims it contains. If the token is valid, the server processes the request and grants access to the requested resource. Token Expiration and Renewal: Tokens are usually issued with an expiration time to limit their validity period. Once a token expires, the user must re-authenticate to obtain a new token. Some systems use refresh tokens, which are long-lived tokens that can be used to obtain new access tokens without requiring the user to log in again. Security Measures: To ensure the security of WATs, several best practices are implemented: Encryption: Sensitive information within the token can be encrypted to protect user data. Signature Verification: Tokens are signed to ensure their integrity and authenticity. The server uses the same secret key (or public key in asymmetric encryption) to verify the token's signature. Secure Storage: Tokens should be stored securely on the client side, avoiding exposure to client-side scripts to prevent XSS attacks. Token Expiration: Tokens have an expiration time to limit the window of opportunity for misuse. Short-lived access tokens and long-lived refresh tokens balance security and user convenience. Scope and Audience Restrictions: Tokens can include claims that restrict their usage to specific scopes and audiences, enhancing security by ensuring tokens are used only as intended. By following these practices, WATs provide a scalable and secure method of managing user authentication and authorization across web applications, improving both performance and user experience. Pros and Cons of WAT Pros Cons Enhanced Security: Tokens are typically signed and can be encrypted, protecting against tampering and unauthorized access. Reduces the risk of session hijacking compared to traditional session management. Scalability: The stateless nature of tokens reduces server load and improves scalability. Servers do not need to maintain a session state, allowing for better performance under high load. Flexibility: Tokens can be used across different platforms, including mobile and single-page applications. Provides a consistent authentication mechanism for diverse environments. Fine-Grained Access Control: Tokens can contain detailed information about user permissions, enabling fine-grained access control and authorization. Allows for dynamic and context-sensitive access management. Decentralized Verification: Authentication and authorization can be performed without a centralized session store, enhancing reliability. Can easily integrate with microservices and distributed systems.   Token Management Challenges: Ensuring secure storage and handling of tokens on the client side is crucial to prevent token theft and misuse. Tokens stored in browser storage can be vulnerable to cross-site scripting (XSS) attacks. Expiration and Renewal: Tokens need to be regularly renewed to maintain security, adding complexity to session management. Handling token expiration and refresh workflows can be complex. Implementation Complexity: Implementing a token-based authentication system requires in-depth knowledge of security practices and protocols. Requires careful planning to ensure security and efficiency. Compatibility Issues: Ensuring that all applications and services can correctly handle and validate tokens can be challenging. Integration with legacy systems may require significant modifications. Performance Overhead: Token verification, especially with cryptographic signatures, adds computational overhead. It can impact performance if not managed properly, especially in high-load environments. Useful Materials: Sisense WAT Documentation Sisense Composed SDK Authentication & Security Documentation Integrating SSO and WAT Combining SSO and WAT can provide a comprehensive authentication and authorization solution. SSO can handle the initial authentication process, while WATs can manage user sessions and permissions across different applications. This integration allows organizations to leverage the strengths of both technologies, providing a seamless and secure user experience. In the next sections, we will explore real-world examples, best practices, and implementation strategies for integrating SSO and WAT in web applications. About Paldi Solutions is a trusted Sisense Gold Partner, specializing in advanced plugins, customized solutions, reporting automation, and embedded analytics enhancements. As a leading Sisense partner, we serve over 160 Sisense clients with enterprise-ready solutions designed for reliability and seamless performance. Our OEM-friendly plugins integrate effortlessly, ensuring scalability and long-term stability. With deep technical expertise and a commitment to innovation, we empower businesses to maximize their BI potential with powerful, user-friendly tools. Discover how Paldi Solutions can elevate your Sisense experience at  www.paldi.solutions  

      Benji_PaldiTeam
      Benji_PaldiTeamPosted 1 year ago
      0
               
    • Blog banner
      • Widget & Dashboard ScriptsChevronRightIcon

      Setting Date Filter Members to Specific Days of the Week in Sisense Programmatically

                                                                               

      Setting Date Filter Members to Specific Days of the Week in Sisense Programmatically Sisense natively supports a wide range of date filter functionalities and formats. However, certain scenarios may call for unusual custom programmatic forms of date filtering, such as filtering to specific days of the week within a defined date range. For example, focusing a dashboard view on only Wednesdays in a given period, or on weekend dates, can yield valuable insights in time-sensitive analyses. This cannot be directly configured with standard Sisense UI filters when only one date dimension is present in the data source. In the previous article , the concept of date calculations was demonstrated by leveraging JavaScript date objects , modifying filter parameters programmatically via  dashboard or widget scripting , and converting those date objects into formats Sisense expects. (For reference, see: Using Native Javascript Date Calculations To Modify Sisense Date Filters ) The same principle can be applied to "Day of the Week" based filtering. Sisense allows defining a custom set of date members as a filter. By dynamically constructing a list of dates that fall on certain days of the week within a chosen date range, it is straightforward to pass these custom members into Sisense filters, setting them as members of a date member filter and creating a selective date filter. In the snippet below, JavaScript generates a list of dates falling on specified weekdays—such as Saturdays and Sundays—within a given start and end date range. It then formats them into the string format Sisense expects for filter members. These can be applied within the filter object just as demonstrated in the previously linked article on programmatic date filter modification.       function getDatesOnWeekdays(weekdays, startDate, endDate) { const dayNameToNumber = { "Sunday": 0, "Monday": 1, "Tuesday": 2, "Wednesday": 3, "Thursday": 4, "Friday": 5, "Saturday": 6 }; // Convert weekday names to their numeric values const weekdaysNumbers = weekdays.map(day => dayNameToNumber[day]); // Convert startDate and endDate to Date objects let start = new Date(startDate); let end = new Date(endDate); // Initialize result array const result = []; // Iterate from startDate to endDate for (let date = new Date(start); date <= end; date.setDate(date.getDate() + 1)) { // Check if the day of the week matches the specified weekdays if (weekdaysNumbers.includes(date.getDay())) { // Format date as "YYYY-MM-DDT00:00:00" const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const formattedDate = `${year}-${month}-${day}T00:00:00`; result.push(formattedDate); } } return result; } // Example usage: const weekdays = ["Saturday", "Sunday"]; const startDate = "2024-11-01"; const endDate = "2024-11-30"; const dates = getDatesOnWeekdays(weekdays, startDate, endDate); console.log(dates); ​     This example produces a list of dates that occur only on Saturdays and Sundays within the specified date range, formatted as YYYY-MM-DDT00:00:00, which is the format Sisense used for date filters. For example: ["2024-11-02T00:00:00", "2024-11-03T00:00:00", "2024-11-09T00:00:00", "2024-11-10T00:00:00"].   Once this list is generated, set the members property of the filter’s JAQL object to these values.       // Assuming modifiedFilter is a date filter object already obtained from the dashboard filters modifiedFilter.jaql.filter.members = dates;     The dashboard filter now includes exclusively the targeted weekdays. This approach depends on programmatic customization. Native Sisense filters do not include a direct "Day of the Week" capability, but a script can calculate the exact set of valid dates meeting any specific criteria, which can then be used with Sisense filter objects to modify the filter programmatically. Since the code directly modifies the filter’s member list, it can be combined with previously illustrated techniques for dynamically setting “From” and “To” dates, offsetting filters based on current or relative dates, and applying other conditional logic. For example, more complex logic could be implemented to filter only the first Tuesday of each month or other patterns. For even more flexibility and to avoid custom code, consider creating a custom field in the underlying data model that stores the day of the week for each date, enabling direct filtering by that custom dimension in the standard Sisense UI. This approach trades off some versatility for simpler maintenance and configuration. As shown, the same principles that enable filters to be dynamically set to certain date ranges can also be applied to filter on "Day of the Week" criteria. This builds on the earlier article’s demonstration of using JavaScript date calculations to craft custom filter conditions, offering an even wider range of data filtering possibilities.   Related Content:   Docs:  https://docs.sisense.com/main/SisenseLinux/customizing-sisense-using-code.htm   Dev:  https://sisense.dev/guides/customJs/extensions/   Sisense Academy:  https://academy.sisense.com/master-class-advanced-dashboards-with-plug-ins-and-scripts  

      Jeremy Friedel
      Jeremy FriedelPosted 1 year ago
      0