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
    Sisense.JS
    • Blog banner
      • Embedding AnalyticsChevronRightIcon

      Adding an expand widget button in SisenseJS embedding [Linux]

                       

      Introduction:  An article demonstrates a way to enhance SisenseJS embedded dashboards by adding an "Expand" widget button. This allows to use the space more efficiently by embedding smaller versions of widgets but granting users with an option to view larger, detailed versions of charts in a modal, improving data readability and user experience. Step-by-Step Guide: Step 1: HTML layout  A simple structure with a main container to render widgets in, div elements for overlay, a modal, a widget container inside a modal, and a button as a close icon. <body>   <div id="sisenseApp" style="display: flex; width:100%">     <main id="main" style="display: flex; flex-direction: column; justify-content: center; width: 80%">              <!-- The Modal Overlay -->       <div class="overlay">         <div class="modal">           <div class="modalWidgetContainer"></div>           <button class="close-btn">              <svg>...</svg> <!-- Close Icon -->           </button>         </div>       </div>            </main>   </div> </body> Step 2: Basic styles for the Modal  Add CSS to handle the visibility of the modal window and define the sizing for widget containers. The .isOpen class will be toggled dynamically later. CSS <style>   .overlay {     position: fixed;     inset: 0;     background-color: rgba(0, 0, 0, 0.2);     visibility: hidden;     z-index: 3;   }   .modal {     position: absolute;     inset: 5%;     background-color: white;   }   .close-btn {     position: absolute;     top: 5px;     right: 0;     cursor: pointer;     z-index: 5;   }   .isOpen {     opacity: 1;     visibility: visible;   }   .primaryWidgetContainer {     width: 400px;     height: 400px;     border: 1px solid sandybrown;   }   .modalWidgetContainer {     width: 100%;     height: 100%;     border: 1px solid sandybrown;   } </style> Step 3: Connect to SisenseJS and Render Widgets  Dynamically load the Sisense.v1.js library, connect to Sisense instance, and fetch the widgets associated with a specific dashboard ID. For each widget loaded, we generate an "Expand" button. javascript const url = document.location.origin; const dashboardId = "697aa04c63601f8c9e5a478c"; // Replace with your dashboard ID const startSisense = async () => { const sisensejs = document.createElement('script'); sisensejs.src = url + '/js/sisense.v1.js'; sisensejs.onload = async () => { await renderdash(); } document.head.append(sisensejs); } const renderdash = async () => { const main = document.getElementById("main"); let app = window.Sisense.app; if (!app) { app = await Sisense.connect(url, true); window.Sisense.app = app; } let dash = new Dashboard(); app.dashboards.add(dash); // Fetch widgets from the dashboard const widgetsRaw = await fetch(url + "/api/v1/dashboards/" + dashboardId + "/widgets?fields=oid", { method: 'GET', credentials: 'include' }).then(r => r.json()); if (widgetsRaw.length) { const promises = widgetsRaw.map(w => dash.widgets.load(w.oid)); const widgetsResolved = await Promise.all(promises); widgetsResolved.forEach(widget => { const groupWrapper = document.createElement('div'); const widgetContainerEl = document.createElement('div'); widgetContainerEl.classList.add('primaryWidgetContainer'); widgetContainerEl.setAttribute("id", "widget_" + widget.$$model.oid); const expandBtnEl = document.createElement('button'); expandBtnEl.textContent = "Expand"; // Bind the modal open event with the current widget context expandBtnEl.addEventListener('click', () => handleModalOpen(widget)); main.append(groupWrapper); groupWrapper.append(widgetContainerEl, expandBtnEl); // Render widget into primary container widget.container = document.getElementById("widget_" + widget.$$model.oid); }); dash.refresh(); } } startSisense(); Step 4: Handling the Modal State and Widget Re-rendering   A SisenseJS widget cannot be simultaneously rendered in two different elements in the DOM. Therefore, when opening the modal, we must first destroy() the widget in the main view and initialize() it inside the modal container. We reverse this process when the user closes the modal. const overlayEl = document.querySelector('.overlay'); const closeModalBtn = document.querySelector('.close-btn'); const modalWidgetContainer = document.querySelector('.modalWidgetContainer'); let activeWidget = null; const handleModalOpen = (w) => {   overlayEl.classList.add('isOpen');   // Destroy the widget and repopulate inside the modal   w.destroy();   w.initialize();   w.container = modalWidgetContainer;      closeModalBtn.addEventListener('click', handleModalClose);   activeWidget = w; } const handleModalClose = () => {   overlayEl.classList.remove('isOpen');      if (activeWidget) {     // Reverse the process to push the widget back to the main view     activeWidget.destroy();     activeWidget.initialize();     activeWidget.container = document.getElementById("widget_" + activeWidget.$$model.oid);   }      closeModalBtn.removeEventListener('click', handleModalClose);   activeWidget = null; } (Full index.html can be found in the comments section) Note: This feature may be beneficial for all native chart types, such as Column, Line, Bar Charts, Scatter Plot/Map, etc. The exceptions are Pivot and Indicator widgets, which don’t get properly expanded. Note: The example doesn’t include authentication and is designed to be tested within the Sisense itself. Upload the HTML file into /opt/sisense/storage/plugins and it will be accessible at your_sisense_url/plugins/sisensejs.html Conclusion:   Adding an expansion feature is an effective way to improve the user experience with embedded dashboards, allowing clients to examine deeper chart details. By understanding that SisenseJS widgets require destruction and re-initialization before being assigned to a new DOM container, you can successfully move them freely across your application while maintaining high interactivity and performance. References/Related Content  https://developer.sisense.com/guides/embeddingCharts/sisense.js/ https://developer.sisense.com/guides/embeddingCharts/jsGettingStarted.html 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.

      Ivan Amoshyi
      Ivan AmoshyiPosted 1 month ago • Last reply 1 month ago
      1
               
    • Blog banner
      • Embedding AnalyticsChevronRightIcon

      Adding an expand widget button in SisenseJS embedding [Linux]

                                       

      Introduction:  An article demonstrates a way to enhance SisenseJS embedded dashboards by adding an "Expand" widget button. This allows to use the space more efficiently by embedding smaller versions of widgets but granting users with an option to view larger, detailed versions of charts in a modal, improving data readability and user experience. Step-by-Step Guide: Step 1: HTML layout  A simple structure with a main container to render widgets in, div elements for overlay, a modal, a widget container inside a modal, and a button as a close icon. <body>   <div id="sisenseApp" style="display: flex; width:100%">     <main id="main" style="display: flex; flex-direction: column; justify-content: center; width: 80%">              <!-- The Modal Overlay -->       <div class="overlay">         <div class="modal">           <div class="modalWidgetContainer"></div>           <button class="close-btn">              <svg>...</svg> <!-- Close Icon -->           </button>         </div>       </div>            </main>   </div> </body> Step 2: Basic styles for the Modal  Add CSS to handle the visibility of the modal window and define the sizing for widget containers. The .isOpen class will be toggled dynamically later. CSS <style>   .overlay {     position: fixed;     inset: 0;     background-color: rgba(0, 0, 0, 0.2);     visibility: hidden;     z-index: 3;   }   .modal {     position: absolute;     inset: 5%;     background-color: white;   }   .close-btn {     position: absolute;     top: 5px;     right: 0;     cursor: pointer;     z-index: 5;   }   .isOpen {     opacity: 1;     visibility: visible;   }   .primaryWidgetContainer {     width: 400px;     height: 400px;     border: 1px solid sandybrown;   }   .modalWidgetContainer {     width: 100%;     height: 100%;     border: 1px solid sandybrown;   } </style> Step 3: Connect to SisenseJS and Render Widgets  Dynamically load the Sisense.v1.js library, connect to Sisense instance, and fetch the widgets associated with a specific dashboard ID. For each widget loaded, we generate an "Expand" button. javascript const url = document.location.origin; const dashboardId = "697aa04c63601f8c9e5a478c"; // Replace with your dashboard ID const startSisense = async () => { const sisensejs = document.createElement('script'); sisensejs.src = url + '/js/sisense.v1.js'; sisensejs.onload = async () => { await renderdash(); } document.head.append(sisensejs); } const renderdash = async () => { const main = document.getElementById("main"); let app = window.Sisense.app; if (!app) { app = await Sisense.connect(url, true); window.Sisense.app = app; } let dash = new Dashboard(); app.dashboards.add(dash); // Fetch widgets from the dashboard const widgetsRaw = await fetch(url + "/api/v1/dashboards/" + dashboardId + "/widgets?fields=oid", { method: 'GET', credentials: 'include' }).then(r => r.json()); if (widgetsRaw.length) { const promises = widgetsRaw.map(w => dash.widgets.load(w.oid)); const widgetsResolved = await Promise.all(promises); widgetsResolved.forEach(widget => { const groupWrapper = document.createElement('div'); const widgetContainerEl = document.createElement('div'); widgetContainerEl.classList.add('primaryWidgetContainer'); widgetContainerEl.setAttribute("id", "widget_" + widget.$$model.oid); const expandBtnEl = document.createElement('button'); expandBtnEl.textContent = "Expand"; // Bind the modal open event with the current widget context expandBtnEl.addEventListener('click', () => handleModalOpen(widget)); main.append(groupWrapper); groupWrapper.append(widgetContainerEl, expandBtnEl); // Render widget into primary container widget.container = document.getElementById("widget_" + widget.$$model.oid); }); dash.refresh(); } } startSisense(); Step 4: Handling the Modal State and Widget Re-rendering   A SisenseJS widget cannot be simultaneously rendered in two different elements in the DOM. Therefore, when opening the modal, we must first destroy() the widget in the main view and initialize() it inside the modal container. We reverse this process when the user closes the modal. const overlayEl = document.querySelector('.overlay'); const closeModalBtn = document.querySelector('.close-btn'); const modalWidgetContainer = document.querySelector('.modalWidgetContainer'); let activeWidget = null; const handleModalOpen = (w) => {   overlayEl.classList.add('isOpen');   // Destroy the widget and repopulate inside the modal   w.destroy();   w.initialize();   w.container = modalWidgetContainer;      closeModalBtn.addEventListener('click', handleModalClose);   activeWidget = w; } const handleModalClose = () => {   overlayEl.classList.remove('isOpen');      if (activeWidget) {     // Reverse the process to push the widget back to the main view     activeWidget.destroy();     activeWidget.initialize();     activeWidget.container = document.getElementById("widget_" + activeWidget.$$model.oid);   }      closeModalBtn.removeEventListener('click', handleModalClose);   activeWidget = null; } (Full index.html can be found in the comments section) Note: This feature may be beneficial for all native chart types, such as Column, Line, Bar Charts, Scatter Plot/Map, etc. The exceptions are Pivot and Indicator widgets, which don’t get properly expanded. Note: The example doesn’t include authentication and is designed to be tested within the Sisense itself. Upload the HTML file into /opt/sisense/storage/plugins and it will be accessible at your_sisense_url/plugins/sisensejs.html Conclusion:   Adding an expansion feature is an effective way to improve the user experience with embedded dashboards, allowing clients to examine deeper chart details. By understanding that SisenseJS widgets require destruction and re-initialization before being assigned to a new DOM container, you can successfully move them freely across your application while maintaining high interactivity and performance. References/Related Content  https://developer.sisense.com/guides/embeddingCharts/sisense.js/ https://developer.sisense.com/guides/embeddingCharts/jsGettingStarted.html 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.

      Ivan Amoshyi
      Ivan AmoshyiPosted 1 month ago
      0
               
    • Blog banner
      • Embedding AnalyticsChevronRightIcon

      Adding an expand widget button in SisenseJS embedding [Linux]

                                       

      Introduction:  An article demonstrates a way to enhance SisenseJS embedded dashboards by adding an "Expand" widget button. This allows to use the space more efficiently by embedding smaller versions of widgets but granting users with an option to view larger, detailed versions of charts in a modal, improving data readability and user experience. Step-by-Step Guide: Step 1: HTML layout  A simple structure with a main container to render widgets in, div elements for overlay, a modal, a widget container inside a modal, and a button as a close icon. <body>   <div id="sisenseApp" style="display: flex; width:100%">     <main id="main" style="display: flex; flex-direction: column; justify-content: center; width: 80%">              <!-- The Modal Overlay -->       <div class="overlay">         <div class="modal">           <div class="modalWidgetContainer"></div>           <button class="close-btn">              <svg>...</svg> <!-- Close Icon -->           </button>         </div>       </div>            </main>   </div> </body> Step 2: Basic styles for the Modal  Add CSS to handle the visibility of the modal window and define the sizing for widget containers. The .isOpen class will be toggled dynamically later. CSS <style>   .overlay {     position: fixed;     inset: 0;     background-color: rgba(0, 0, 0, 0.2);     visibility: hidden;     z-index: 3;   }   .modal {     position: absolute;     inset: 5%;     background-color: white;   }   .close-btn {     position: absolute;     top: 5px;     right: 0;     cursor: pointer;     z-index: 5;   }   .isOpen {     opacity: 1;     visibility: visible;   }   .primaryWidgetContainer {     width: 400px;     height: 400px;     border: 1px solid sandybrown;   }   .modalWidgetContainer {     width: 100%;     height: 100%;     border: 1px solid sandybrown;   } </style> Step 3: Connect to SisenseJS and Render Widgets  Dynamically load the Sisense.v1.js library, connect to Sisense instance, and fetch the widgets associated with a specific dashboard ID. For each widget loaded, we generate an "Expand" button. javascript const url = document.location.origin; const dashboardId = "697aa04c63601f8c9e5a478c"; // Replace with your dashboard ID const startSisense = async () => { const sisensejs = document.createElement('script'); sisensejs.src = url + '/js/sisense.v1.js'; sisensejs.onload = async () => { await renderdash(); } document.head.append(sisensejs); } const renderdash = async () => { const main = document.getElementById("main"); let app = window.Sisense.app; if (!app) { app = await Sisense.connect(url, true); window.Sisense.app = app; } let dash = new Dashboard(); app.dashboards.add(dash); // Fetch widgets from the dashboard const widgetsRaw = await fetch(url + "/api/v1/dashboards/" + dashboardId + "/widgets?fields=oid", { method: 'GET', credentials: 'include' }).then(r => r.json()); if (widgetsRaw.length) { const promises = widgetsRaw.map(w => dash.widgets.load(w.oid)); const widgetsResolved = await Promise.all(promises); widgetsResolved.forEach(widget => { const groupWrapper = document.createElement('div'); const widgetContainerEl = document.createElement('div'); widgetContainerEl.classList.add('primaryWidgetContainer'); widgetContainerEl.setAttribute("id", "widget_" + widget.$$model.oid); const expandBtnEl = document.createElement('button'); expandBtnEl.textContent = "Expand"; // Bind the modal open event with the current widget context expandBtnEl.addEventListener('click', () => handleModalOpen(widget)); main.append(groupWrapper); groupWrapper.append(widgetContainerEl, expandBtnEl); // Render widget into primary container widget.container = document.getElementById("widget_" + widget.$$model.oid); }); dash.refresh(); } } startSisense(); Step 4: Handling the Modal State and Widget Re-rendering   A SisenseJS widget cannot be simultaneously rendered in two different elements in the DOM. Therefore, when opening the modal, we must first destroy() the widget in the main view and initialize() it inside the modal container. We reverse this process when the user closes the modal. const overlayEl = document.querySelector('.overlay'); const closeModalBtn = document.querySelector('.close-btn'); const modalWidgetContainer = document.querySelector('.modalWidgetContainer'); let activeWidget = null; const handleModalOpen = (w) => {   overlayEl.classList.add('isOpen');   // Destroy the widget and repopulate inside the modal   w.destroy();   w.initialize();   w.container = modalWidgetContainer;      closeModalBtn.addEventListener('click', handleModalClose);   activeWidget = w; } const handleModalClose = () => {   overlayEl.classList.remove('isOpen');      if (activeWidget) {     // Reverse the process to push the widget back to the main view     activeWidget.destroy();     activeWidget.initialize();     activeWidget.container = document.getElementById("widget_" + activeWidget.$$model.oid);   }      closeModalBtn.removeEventListener('click', handleModalClose);   activeWidget = null; } (Full index.html can be found in the comments section) Note: This feature may be beneficial for all native chart types, such as Column, Line, Bar Charts, Scatter Plot/Map, etc. The exceptions are Pivot and Indicator widgets, which don’t get properly expanded. Note: The example doesn’t include authentication and is designed to be tested within the Sisense itself. Upload the HTML file into /opt/sisense/storage/plugins and it will be accessible at your_sisense_url/plugins/sisensejs.html Conclusion:   Adding an expansion feature is an effective way to improve the user experience with embedded dashboards, allowing clients to examine deeper chart details. By understanding that SisenseJS widgets require destruction and re-initialization before being assigned to a new DOM container, you can successfully move them freely across your application while maintaining high interactivity and performance. References/Related Content  https://developer.sisense.com/guides/embeddingCharts/sisense.js/ https://developer.sisense.com/guides/embeddingCharts/jsGettingStarted.html 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.

      Ivan Amoshyi
      Ivan AmoshyiPosted 1 month ago
      0
               
    • Blog banner
      • Add-ons & Plug-InsChevronRightIcon

      Plugin - Custom Coloring of Bar and Column Chart Type Widgets (barChartCustomColor)

                                                       

      Plugin - Custom Coloring of Bar and Column chart-type widgets This plugin  modifies the color scheme of bar and column chart-type widgets to match the current color palette of the dashboard. Installation To install this plugin, download and unzip the attachment. Then drop the barChartCustomColor folder into your plugins folder (/opt/sisense/storage/plugins). Enable the plugin on the Add-ons tab in the Admin section, wait for the plugin to build, and the plugin will be enabled. The plugin API can also be used, as well as the file management UI . Notes When a bar or column chart type Sisense widget is very straightforward and focused and does not have multiple values, breaks by's, categories, or conditional coloring, it can sometimes appear relatively mono-color but still be the most effective way to quickly visually communicate important data.   While Sisense includes numerous powerful and varied methods and options to change widget styling  and includes functionality to set bar color to vary based on the bar value , it sometimes may be desired to use the dashboard palette to vary the bar colors independently of bar value and guarantee a colorful widget regardless of the data and current filter state of the current dashboard and widget. This plugin allows the current dashboard palette to be used in this manner in a simple and quick matter and allows this to be applied quickly to an entire dashboard at once. Linking the widget coloring to the dashboard palette allows this color scheme to be quickly changed in the native UI without modifying individual widgets. This plugin also allows using a color dictionary config option to modify the coloring of a value in all widgets this plugin is enabled in.       This plugin  uses the processresult widget event to modify the color parameter of each bar or column in a series.   The dashboard palette is retrieved using the getPalette() function within the dashboard style parameter, which returns the current dashboard palette. This can be used in other plugins and scripts.     dashboard.style.getPalette()       This plugin is enabled via dashboard or widget scripts that set the widget parameter changeColor to true for a widget.   For a widget script, this is as simple as adding this one line to the script:     widget.changeColor = true;       Adding this three-line dashboard script will set this parameter to true for all widgets in the dashboard:     dashboard.on('widgetinitialized', function (_, dashObj) { dashObj.widget.changeColor = true; });     This can be modified with custom logic as needed, for example, based on widget title or ordering in the dashboard. If this option is enabled for a type of widget this plugin does not apply to, it will simply be ignored by the plugin. This plugin ignores widgets with  Break By's or Date Categories, or widgets that are not bar or column chart-type widgets. The color scheme is based on the current dashboard palette.    Changing the dashboard palette using the standard Sisense palette UI will result in a matching change of all widgets in the dashboard where this plugin is enabled. The config file of this plugin includes an option called colorDictionary, this can be used to override the standard palette color for all instances this value appears as the bar category, for all widgets this plugin is enabled for, regardless of the dashboard palette. Any standard HTML color naming format may be used in the dictionary config value. For example, if the colorDictionary is set to:     colorDictionary : { 'Bikes': '#AA6C39', 'ABC' : 'yellow' }      Then the ABC bar or column will be yellow in the widget, regardless of the current widget palette or positioning. This will apply to all widgets where this plugin is active. This plugin can be used as a basic template for your own Sisense plugins that run on a specific widget or dashboard event, that can be enabled or disabled via widget script, and that includes a configuration file to modify settings without modifying code files.        How did the plugin work for you? What other type of plugin are you looking to learn more about? Let me know in the comments!

      Jeremy Friedel
      Jeremy FriedelPosted 2 years ago • Last reply 2 months ago
      1
               
    • 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 2 months ago
      1
               
      • TroubleshootingChevronRightIcon

      Mastering custom actions in Sisense: debugging and understanding payload properties

                                                               

      Mastering custom actions in Sisense: debugging and understanding payload properties When you use a custom action from the community, develop your own, or try to understand why your action does not work as expected, sometimes you need to change the custom action’s logic. Maintaining someone’s code or developing your code is not easy when the code is executed in a 3rd party environment (Sisense). In this article, I will explain what properties are passed to the action and how to debug custom actions. When you need to understand what goes wrong, use a magic word debugger . When the browser’s developer tool is open, the code’s execution will be stopped on this code’s line. Let’s create a simple action with a single line of the code: debugger As the action’s snippet use the following one: {     "type" : "testAction" ,     "title" : "Test action" ,     "data" : {         "firstArgument" : "string" ,         "secondArgument" : "boolean"    } } Use this snippet as an action. As a result, you will see a new button on your widget. Open the developer tool of your browser and click on this button. The code’s execution was stopped on the line debugger and you will see the following code: (function anonymous(payload) {     debugger; })  If you hover over the variable payload , you will see its value. Using this argument, you can get access to other parts of the currently opened dashboard. For example, the dashboard’s filters are located in the following path: payload.widget.dashboard.filters Also, you can get other widgets from the dashboard, using the following construction: payload.widget.dashboard.widgets.get(widgetId) In this logic, the variable widgetId is the identifier of the widget you want to get from the current dashboard. Word debugger can be added to almost any line of your action, so you can debug your logic. If the code’s execution was not stopped in the  debugger , there can be the following reasons: Some issues occurred earlier in the custom action, so you need to place the  debugger earlier; The custom action was not called, because it does not exist. In this case, you will see the following message in the console: BloX Alert!  The action type was not found. Breakpoints are deactivated: [ALT text: Screenshot of a debugging interface showing a toolbar at the top with various icons. An arrow points to the "Deactivate breakpoints" icon, accompanied by a tooltip that indicates the keyboard shortcut (Command + F8) for the action. Below, there are sections labeled "Breakpoints," "Scope," and "Call Stack," with options and statuses displayed.]     Conclusion: A summary of key takeaways or final points. Using this approach you can easily develop your actions.   References/Related Content: Links and resources for further reading. https://docs.sisense.com/main/SisenseLinux/creating-custom-actions.htm https://sisense.dev/guides/customJs/jsApiRef/widgetClass https://sisense.dev/guides/customJs/jsApiRef/dashboardClass DO NOT CHANGE!!! 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.  

      Oleksii Demianyk
      Oleksii DemianykPosted 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
               
    • Blog banner
      • Widget & Dashboard ScriptsChevronRightIcon

      Ensuring Accurate PDF Export Headers When Programmatically Modifying Dashboard Filters in Sisense

                                                                               

      When working with Sisense dashboards, programmatically modifying filters upon dashboard load via  dashboard scripts  for a specific user or circumstance is possible . However, an issue can sometimes occur when exporting these dashboards to PDF immediately after such modifications: the filter header displayed at the top of the exported PDF displaying the current dashboard filter state may not accurately reflect the current state of the dashboard filters. This article explains the cause of this issue and presents a solution to ensure that PDF exports display the correct filter information in the header. If this issue is not currently occurring, then the existing custom widget or dashboard script does not need to be modified, this step is only required in certain specific circumstances. Inconsistencies in PDF Filter Headers Programmatic modifications to dashboard filters during the initial load may not be fully registered in the dashboard's state when an export to PDF is initiated immediately. If the dashboard is exported before any manual interaction, such as refreshing or adjusting filters, the PDF's filter header might display outdated or incorrect filter information. This occurs because the dashboard's internal state and the visual representation of filters may not be fully synchronized immediately after the programmatic changes. The export function relies on the dashboard's state at the time of export, and if this state hasn't been properly updated to include the new filters, the PDF will reflect the previous filter settings. No Filters Visible In PDF Header   Filter Visible in PDF Header   Solution: Updating the Dashboard State to Update PDF Filter Header To resolve this issue, explicitly updating the dashboard's state after programmatically modifying the filters ensures that all components of the dashboard, including the export functionality, are aware of the current filter settings. By adding the following line of code after the filter modifications, the dashboard is forced to update its state to include the new filters: prism.activeDashboard.$dashboard.updateDashboard(prism.activeDashboard, ['filters']); Explanation prism.activeDashboard: Refers to the currently active dashboard instance. The prism object is documented here . $dashboard.updateDashboard(): A method that updates the dashboard's state. Parameters: First Parameter (prism.activeDashboard): The dashboard instance to update. Second Parameter (['filters']): An array specifying which parts of the dashboard to update; in this case, the filters. By specifying ['filters'], only the filters are updated, which is efficient and avoids unnecessary updates to other dashboard components. Implementation Steps Modify Filters Programmatically : Ann existing script to change the dashboard filters as required upon loading. dashboard.filters.$$items[0].jaql.filter.members = ["Current User"] Update the Dashboard State : Immediately after modifying the filters, add the update line. // Update the dashboard to reflect filter changes dashboard.$dashboard.updateDashboard(prism.activeDashboard, ['filters']); Export to PDF : Proceed with exporting the dashboard to PDF. The filter header should now accurately display the current filter settings. Conclusion By ensuring the dashboard's state is updated after programmatic filter changes, any exports or other state-dependent operations will reflect the true current state of the dashboard. This simple addition to the script can prevent confusion and ensure that exported PDFs accurately represent the intended data and filters. By following the steps outlined, Sisense dashboards will accurately reflect programmatic filter changes in exported PDFs, providing clarity and consistency. Related Content:  Docs:  https://docs.sisense.com/main/SisenseLinux/customizing-sisense-using-code.htm Academy:  https://academy.sisense.com/master-class-advanced-dashboards-with-plug-ins-and-scripts

      Jeremy Friedel
      Jeremy FriedelPosted 1 year ago
      0
               
    • Blog banner
      • Widget & Dashboard ScriptsChevronRightIcon

      Using Native Javascript Date Calculations To Modify Sisense Date Filters

                                                                                               

      Using Native Javascript Date Calculations To Modify Sisense Date Filters Sisense natively supports various types of date filter functionalities. However, there are instances where a dynamically updating date filter is desired based on dynamically changing conditions such as the current date. Such a filter may not precisely align with the filters provided in the Sisense UI. One approach to achieve this custom behavior of a dynamically updating filter is through the use of dashboard or widget scripting . A particular consideration when dealing with a date filter is that, in most cases, date filter calculations are best done using JavaScript date objects (which use Unix time ). Sisense date filters typically utilize dates in standard calendar-based string date formats. The following script exemplifies this functionality; It generates a date object based on the required custom date behavior, converts it into a string format compatible with Sisense, and then modifies the Sisense date filter with the formatted date string. JavaScript date objects offer numerous useful functions, enabling straightforward mathematical operations such as addition or subtraction to calculate dates based on desired formulas. This script example specifically identifies dates a set number of days ago (variables set at 60 and 10 days ago from today, respectively) and sets a date filter using these values in the "From" and "To" parameters of a date range filter.     // Programmatically modify Date dashboard filter "From" value to day to a set number of days ago, and the "To" date to different set number of days ago dashboard.on('initialized', function () { // Modify to match the relevant date filter title, date filter must already exist and be "From" and "To" date filter let filterModifiedName = "Date"; // Number of days ago to find date of from "From" date // Modify to days ago to calculate "From" date var daysAgo = 60 // Number of days ago to find date of from "To" date // Modify to days ago to calculate "To" date var offsetDate = 10 filterModifiedName = filterModifiedName.toLowerCase() // Variable containing the Dashboard filter modified by script let modifiedFilter = dashboard.filters.$$items.find(function (filterItem) { return !filterItem.isCascading && filterItem.jaql && filterItem.jaql.title && filterItem.jaql.title.toLowerCase() === filterModifiedName; }); // If relevant filter has from and to values selected, date filter must already exist and have from and to values if (modifiedFilter && modifiedFilter.jaql && modifiedFilter.jaql.filter && modifiedFilter.jaql.filter.from && modifiedFilter.jaql.filter.to && daysAgo && daysAgo >= 1) { // "From" date variable var fromDate = new Date // Time in MS of number of daysAgo var daysAgoInUnixTimeUnits = daysAgo * 24 * 60 * 60 * 1000; // Subtract daysAgo in MS to find date daysAgo ago var daysAgoDateUnixTime = fromDate.getTime() - daysAgoInUnixTimeUnits; // Set date object of date in unix time fromDate.setTime(daysAgoDateUnixTime) // Format date for Sisense date format var formattedFromDate = fromDate.toISOString().slice(0, 10) // Set date filter "From" value modifiedFilter.jaql.filter.from = formattedFromDate // "To" date variable var toDate = new Date // Time in MS of number of offsetDate var daysAgoInUnixTimeUnitsOffset = offsetDate * 24 * 60 * 60 * 1000; // Subtract offsetDate in MS to find date var daysAgoDateUnixTimeTo = toDate.getTime() - daysAgoInUnixTimeUnitsOffset; // Set date object of date in unix time toDate.setTime(daysAgoDateUnixTimeTo) // Format date for Sisense date format var formattedToDate = toDate.toISOString().slice(0, 10) // Set date filter "To" value modifiedFilter.jaql.filter.to = formattedToDate } });     This script is a slight variation that always sets the "To" date to the current date without offsetting this parameter:     // Programmatically modify Date dashboard filter "From" value to day to a set number of days ago, and the "To" date to current date dashboard.on('initialized', function () { // Modify to match the relevant date filter title, date filter must already exist and be "From" and "To" date filter let filterModifiedName = "Date"; // Number of days ago to find date of // Modify to days ago to calculate "From" date var daysAgo = 60 filterModifiedName = filterModifiedName.toLowerCase() // Variable containing the Dashboard filter modified by script let modifiedFilter = dashboard.filters.$$items.find(function (filterItem) { return !filterItem.isCascading && filterItem.jaql && filterItem.jaql.title && filterItem.jaql.title.toLowerCase() === filterModifiedName; }); // If relevant filter has from and to values selected, date filter must already exist and have from and to values if (modifiedFilter && modifiedFilter.jaql && modifiedFilter.jaql.filter && modifiedFilter.jaql.filter.from && modifiedFilter.jaql.filter.to && daysAgo && daysAgo >= 1) { // "From" date variable var fromDate = new Date // Time in MS of number of daysAgo var daysAgoInUnixTimeUnits = daysAgo * 24 * 60 * 60 * 1000; // Subtract daysAgo in MS to find date daysAgo ago var daysAgoDateUnixTime = fromDate.getTime() - daysAgoInUnixTimeUnits; // Set date object of date in unix time fromDate.setTime(daysAgoDateUnixTime) // Format date for Sisense date format var formattedFromDate = fromDate.toISOString().slice(0, 10) // Set date filter "From" value modifiedFilter.jaql.filter.from = formattedFromDate // "To" date variable var todayDate = new Date // Format date for Sisense date format var formattedToDate = todayDate.toISOString().slice(0, 10) // Set date filter "To" value modifiedFilter.jaql.filter.to = formattedToDate } });     Breaking down the script into parts, the initial step in modifying a filter (in this case, a dashboard filter, but applicable to a widget filter) is to find the correct filter object in the dashboard array of filter objects . This is achieved by checking for the filter object whose title matches a given variable in this example. It then checks whether the filter is a calendar range-type date filter.     filterModifiedName = filterModifiedName.toLowerCase() // Variable containing the Dashboard filter modified by script let modifiedFilter = dashboard.filters.$$items.find(function (filterItem) { return !filterItem.isCascading && filterItem.jaql && filterItem.jaql.title && filterItem.jaql.title.toLowerCase() === filterModifiedName; });     After a conditional check to ensure the date filter was found, the calculation of the date object is accomplished. First, a JS date object is created based on Unix time, defaulting to the current Unix time in UTC. The date is then modified mathematically by, in this case, subtracting from the current Unix time in milliseconds the set number of days to calculate. The days-to-MS calculation is performed using multiplication (hours in a day, minutes in an hour, seconds in a minute, MS in a second). This method can be easily modified to calculate values for years, months, or other time units as needed. Finally, the date object in MS is converted to an ISO format date string (a human-readable calendar string), and the part of the string containing details about units smaller than the day is removed by a straightforward string splice.     // "From" date variable var fromDate = new Date // Time in MS of number of daysAgo var daysAgoInUnixTimeUnits = daysAgo * 24 * 60 * 60 * 1000; // Subtract daysAgo in MS to find date daysAgo ago var daysAgoDateUnixTime = fromDate.getTime() - daysAgoInUnixTimeUnits; // Set date object of date in unix time fromDate.setTime(daysAgoDateUnixTime) // Format date for Sisense date format var formattedFromDate = fromDate.toISOString().slice(0, 10) // Set date filter "From" value modifiedFilter.jaql.filter.from = formattedFromDate // "To" date variable var todayDate = new Date // Format date for Sisense date format var formattedToDate = todayDate.toISOString().slice(0, 10) // Set date filter "To" value modifiedFilter.jaql.filter.to = formattedToDate     The filter object was already found in the previous step, so the filter object is modified by adjusting the JAQL filter "To" or "From" parameter of the filter object. No additional code is needed to modify the filter in this case; the filter is modified by directly adjusting the existing parameters of the filter JAQL object. In this example, the current date is used as the starting point, so the script applied by this filter will change based on the current date. Below is the result of this filter changing script, using the 60 days to 10 days ago from the current date values set as parameters. Simpler or more complex calculations or formulas could be applied as required to form the dates required for a particular use case. Any date calculation that is programmatically calculable can then be converted to a calendar date format and used to modify a date filter. Native Sisense allows many different types of data filters, and the JAQL syntax can also be used to create custom filters. However, programmatically setting filters allows for complete flexibility in types of dynamically changing date filters. The examples above use only the dashboard "initialized" event, and as such, only modify the set filter when the dashboard is first loaded. As a result, it remains possible to modify the filter normally; the script will only modify the filter again on a fresh load of the dashboard, allowing a user to adjust the filter temporarily as required. While the above example uses the native Sisense filter object, the filter object in Sisense.js is identical to the native Sisense filter object. Therefore, this code can also be used in code determining the date strings to apply with the Sisense.js applyFilter function. Similarly, this type of JS date object calculation can be used to determine the filters to use in a date filter in ComposeSDK, with only the final step of applying the filter differing.   Share your experience in the comments! 

      Jeremy Friedel
      Jeremy FriedelPosted 2 years ago • Last reply 1 year ago
      13
               
      • APIsChevronRightIcon

      Exporting Options in SisenseJS

                       

      Exporting Options in SisenseJS SisenseJS is a powerful tool that allows embedding widgets from Sisense into external applications. Despite it being a very straightforward way of embedding, SisenseJS has some missed functionality. For example, there are no OOTB methods to export widget's data into CSV and Excel. In this article, we will implement this functionality.  We will use a combination of Sisense REST API endpoints for exporting and capabilities of SisenseJS in order to prepare payloads. In our sample, we will create the required logic for one widget. You can replicate this logic for every widget in your implementation. Common logic When you load a dashboard, you need to save its object. For these purposes we will use a variable [currentDashboard]:     Sisense.connect('https://example.com').then(function (app) { app.dashboards.load(dashboardId).then(function (dashboard) { currentDashboard = dashboard; //Further logic to render widgets on the page }); });     In terms of our task we need to get the widget’s object we are going to export. To get this object you can run the following logic:     const getWidget = (widgetId) => { return currentDashboard.widgets.get(widgetId).$$model; }     This function returns the widget’s object. We do have the widget’s object, so we know the widget’s metadata and also we have information about the dashboard’s filters. We could use this to build a query manually, but it would be quicker to utilize the SisenseJS capabilities. When we call the function getWidget() we need to provide the widget’s identifier as an argument:     const currentWidget = getWidget('63cfd64645e8ec00324a13aa'); //Replace "63cfd64645e8ec00324a13aa" with your widget's identfier. Note, that it should be loaded and rendered     As a result, we will get the widget’s model. Later we will use it to prepare the payload. Also, we need to implement some functions to generate unique identifiers. This is not a mandatory step, but it can be useful if you want to cancel queries. Sample of the function:     function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }     Excel To generate payload for Excel we will use the function below:     const prepareExcelPayload = (widget) => { const query = widget.dashboard.$query.buildWidgetQuery(widget); //Use Sisense capabilities to prepare query query.queryGuid = uuidv4(); //Generate queryGuid query.dashboard = widget.dashboardid || widget.dashboard.oid; query.culture = "en-us"; //This can be set from navigator.language query.format = 'pivot'; query.metadata.forEach(function(m) { if (m.jaql && m.jaql.dim) { //We need to remove [table] and [column] delete m.jaql.table; delete m.jaql.column; } }) return JSON.stringify({ query: query, options: {} }); //Return payload for exporting in Excel }     Once the payload is prepared, we can send it to Sisense’s endpoint /engine/excelExport to retrieve the prepared Excel file. After this, we need to load the generated Excel file explicitly. Code:     function getExcel(widget) { const xhttp = new XMLHttpRequest(); xhttp.withCredentials = true; //This will be cross-domain request, so we force the browser to add cookies xhttp.onreadystatechange = function () { if (xhttp.readyState === 4 && xhttp.status === 200) { const a = document.createElement('a'); a.href = window.URL.createObjectURL(xhttp.response); a.download = widget.title ? `${widget.title}.xlsx` : 'Details.xlsx'; a.style.display = 'none'; document.body.appendChild(a); a.click(); } }; xhttp.open("POST", `${sisenseUrl}/engine/excelExport`); xhttp.setRequestHeader("Content-Type", "application/json"); xhttp.responseType = 'blob'; xhttp.send(payload); }     CSV A function to generate query:     const prepareCSVPayload = (widget) => { let query = widget.$query.buildWidgetQuery(widget, 'exportToCSV'); query = Object.assign(query, { format: "csv", isMaskedResponse: true, download: true, count: 0, offset: 0 }); query = widget.dashboard.$query.createJaqlRequestConfig(query); query.metadata.filter((item) => { return defined(item.format); }) return { data: encodeURIComponent(JSON.stringify(query)) }; }     This function returns a payload, which can be sent at the endpoint `/api/datasources/${ widget.datasource.title }/jaql/csv`. As you can see, this endpoint depends on the datasource’s title. Since the dashboard’s datasource can differ from the widgets’ datasources, I do recommend getting the correct datasource from the widget itself and for sure you need to avoid any hardcoded values.     function getCSV(widget) { let csvDownloader = new XMLHttpRequest(); csvDownloader.open('POST', `/api/datasources/${widget.datasource.title}/jaql/csv`); csvDownloader.responseType = 'arraybuffer'; csvDownloader.onload = function () { if (this.status === 200) { let filename = ""; const disposition = csvDownloader.getResponseHeader('Content-Disposition'); if (disposition && disposition.indexOf('attachment') !== -1) { const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; const matches = filenameRegex.exec(disposition); if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, ''); } const type = csvDownloader.getResponseHeader('Content-Type'); const blob = typeof File === 'function' ? new File([this.response], filename, { type: type }) : new Blob([this.response], { type: type }); if (typeof window.navigator.msSaveBlob !== 'undefined') { // IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed." window.navigator.msSaveBlob(blob, filename); } else { const URL = window.URL || window.webkitURL; const downloadUrl = URL.createObjectURL(blob); if (filename) { const a = document.createElement("a"); if (typeof a.download === 'undefined') { window.location = downloadUrl; } else { a.href = downloadUrl; a.download = filename; document.body.appendChild(a); a.click(); } } else { window.location = downloadUrl; } setTimeout(() => { URL.revokeObjectURL(downloadUrl); }, 100); // cleanup } } }; csvDownloader.withCredentials = true; csvDownloader.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); $.param(payload); csvDownloader.send(payload); }   I hope you find this article useful and leverage the knowledge shared about exporting widgets data in different formats. Please share your experience in the comments!

      Oleksii Demianyk
      Oleksii DemianykPosted 3 years ago • Last reply 1 year ago
      5