Disable Wizard Mode for New Widgets
Scenario: embedded Sisense By default, when creating a new widget, Sisense puts the user into the widget "Wizard Mode", which then requires the use to click "Advanced Configuration" to view the full-feature widget editor. We don't want our users to use "Wizard Mode" and to always be placed directly into "Advanced Configuration". This should be a configurable setting at the Admin level.2.4KViews10likes5CommentsEnhance Widget Export API
Add the ability to programmatically export a Widget with specific filters and/or to export it as a certain user using the /dashboard/{id}/widgets{id}/ endpoint. User Story#1 As an admin I want to make api calls to the widget pdf export endpoint and include userid and custom filters so that I can download an export on behalf of another user in the system and specify the filters Acceptance Criteria I can pass 2 properties to the export endpoint: User Id and filters Both parameters are optional and can be used separately from each other If a user Id parameter is provided, the Widget is exported on behalf of the provided user including the current filters applied and data security rules. If a custom filters parameter is provided, the Widget is exported with the custom filters. There is no merge between the existing Widget filters and the custom filters parameter. The is an option to provide an empty custom filters parameter, in this case, no filters are applied to the dashboard. User story #2 As a user (not admin) I want to make api calls to the Widget pdf export endpoint and include custom filters so that I can download an export with custom filters Acceptance Criteria I can pass a custom filters parameter to the export to the Export endpoint The custom filters parameter is a jaql query that will replace the current dashboard filters The custom filters will respect my data security I can provide an empty custom filters parameter that will remove all filters from the report.1.2KViews9likes1CommentBuilding Confidence (and BloX Widgets) with the Help of Sisense Trainers
My personal experience taking two interactive training sessions with Sisense trainers as a new Sisense user. How I’ve been able to apply new skills to my dashboard work and feel more confident in dashboard design and customizing Blox widgets to get exactly the look and feel I want for my customer experience.2.9KViews7likes2CommentsAdd indicator for downloading a widget
While downloading a large csv/excel file (over several hundreds mb file) from the widget, there would be a long delay of couple minutes before the browser indicates the file has been downloaded. During this time, the user has no indication if the request to download is in process or has failed. It would be good to have an indicator to show the download of the file is in progress, so user would not click the download multiple times thus causing multiple queries to hit the backend.271Views5likes1CommentSisense.js Demo with Filter and Dashboard Changing with Native Web Components
This is a Sisense.js Demo built with React, which includes functionality such as changing filters and dashboards with native web components. This can be used as a reference or starting point for building and customizing Sisense.js applications with custom styling and functionality. React Sisense.js Demo Getting Started Make sure CORS is enabled and the location/port your hosting this applicaiton on is whitelisted on Sisense instance, documentation on changing CORS settings is here. Open the terminal (on Mac/Linux search for terminal, on Windows search for Powershell, Command Prompt, or Windows Subsystem for Linux if installed ) and set path to folder containing project (using cd command). Open the config file in the folder src folder and set Sisense Server URL to the IP or domain name of your Sisense server and set the dashboard id's and optionally filters keys. Run the command. npm install && npm start If npm and node is not installed, install them using the package manager or using nvm (Node version manager). Nvm can be installed by running this command in a terminal curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash Once install script is finished, follow any instructions listed in terminal, restart or open a new terminal and run these command to install node and npm: nvm install node nvm install-latest-npm To start hosting the site, run this command: npm install && npm start The default location is IP of hosting server on port 3000. If the IP address of the server hosting this program is 10.50.74.149 for example, then enter 10.50.74.149:3000 in the address bar of a browser to view application. if hosted locally enter your ip on port 3000 or enter localhost:3000 To use a different port, set the port variable in terminal using this command: export PORT=NEW_PORT_NUMBER and then run npm start. To run in the background use PM2 or system. Requirements Npm (and Node) - Installs all requirements Access to a Sisense instance Description This is a demonstration of some of the capabilities of Sisense.js and Sisense. It is a React single-page application using Typescript that can switch between multiple dashboards, loading all widgets in those dashboards to individual elements (created on dashboard switch). Multiple dashboard filters can be changed (and multiple members in a filter) using a native React dropdown bar to filter the data with the new filtered data visualized with the widgets in the dashboard. Changes made to filters using widgets in Sisense.js using right click are reflected in dropdown filter. Connecting to Sisense // Loads Sisense.js from Server, saves Sisense App copy in window as object to indicate already loaded to prevent loading twice const loadSisense = () => { // If Sisense is already loaded stop if (window.sisenseAppObject) { return; } // Loads sisense app object into app variable, edits being saved to dashboard set to false window.Sisense.connect(config.SisenseUrl, config.saveEdits) // Sisense app object .then((app: SisenseAppObject) => { // loads Sisense app object into window object, so connect is only run once, alternative is export and import window.sisenseAppObject = app; // Calls loadDashboard after Sisense is connected, uses initial widget and dashboard variables loadDashboard(config.DashboardConfig[0].DashboardID, config.DashboardConfig[0].DimArray); }) // error catching and log in console .catch((e: Error) => { console.error(e); }); } Loading Dashboard and Creating Container Element for each Widget // Function to load dashboard taking dashboard id as parameter to load dashboard. Every widget in dashboard is rendered into an element created in loop, looks for parent element with ID 'widgets'. On calling again existing widgets are replaced by new ones. const loadDashboard = (dashboardID: string = '') => { // if empty dashboard id return if (dashboardID === '') { return; } // load dashboard into being active dashboard window.sisenseAppObject.dashboards.load(dashboardID) // after load dashboard is in dash variable .then((dash: DashObject) => { window.currentDashObject = dash // array of loaded widgets // let widgetArray: Array<String> = prism.activeDashboard.widgets.toArray().map( let widgetArray: Array<String> = dash.$$widgets.$$widgets.map( function (widget: Widget) { // widget id for loading return widget.id } ); // set state with loaded dashboard if prism loaded if (widgetArray.length > 0) { this.setState((state, props) => { return { dashboardID: dash.id, }; }); } // get widgets element let widgetsElement: HTMLElement | null = document.getElementById(`widgets`); // type checking if (widgetsElement === null) { return; } // erase previous widgets widgetsElement.innerHTML = ''; // loop through array of widget arrays, loads them into containers by id with first in widget1, second in widget2 and so on widgetArray.forEach((widget, index) => { // check if they exist, type checking if (widgetsElement === null) { return; } // element to load widget into later let widgetElement: HTMLElement | null = document.createElement("div"); // Class included index for widget rendering later widgetElement.classList.add(`widget${index + 1}`, 'widget') // add empty div to widgets parent div widgetsElement.appendChild(widgetElement) // get widget and filter elements by ID let filterElement: HTMLElement | null = document.getElementById("filters"); // check if they exist, type checking if (widgetElement === null || filterElement === null) { return; } // Clear widget and filter elements from previous render filterElement.innerHTML = ''; // put widget in container element dash.widgets.get(widget).container = widgetElement; // Renders filter in HTML element with id of filters dash.renderFilters(filterElement); // reloads and refresh dashboard }); dash.refresh(); }) // error catching and log in console .catch((e: Error) => { console.error(e); }); } Changing a Dashboard Filter // Change a dashboard filter, takes dim to filter by, and new values in filter. const changeDashboardFilter = (dashboard: dashboard, dim: string, newFilterArray: Array<String>) => { // Find matching filter and to make changes to let filterToChange = dashboard.filters.$$filters.find(item => item.jaql.dim === `[${dim}]`); // If filter is undefined create a new filter if (filterToChange === undefined) { // Create the filter options let filterOptions = { // Save to dashboard save: false, // Refresh on change refresh: true, // If filter already used, make changes to that filter instead of creating new ones unionIfSameDimensionAndSameType: true }; // Create the jaql for the filter let jaql = { 'datatype': 'text', 'dim': dim, 'filter': { // Multiple items can be selected 'multiSelection': true, // New filter items 'members': newFilterArray, 'explicit': true }, }; // Create the filter jaql object let applyJaql = { jaql: jaql }; // Set the new filter using update function dashboard.$$model.filters.update(applyJaql, filterOptions); } if (filterToChange && filterToChange.$$model.jaql.filter) { let members = filterToChange.$$model.jaql.filter.members; // Check if members exist if (members !== undefined) { // Set members to new selected filter filterToChange.$$model.jaql.filter.members = newFilterArray; // Save the dashboard // dashboard.$$model.$dashboard.updateDashboard(dashboard.currentDashObject.$$model, "filters"); dashboard.filters.update(filterToChange, { refresh: true, save: false }); // Refresh the dashboard // dashboard.refresh(); } } } Clearing a filter Clearing a filter is done by simply setting the members to an empty array, which disables the filter. changeFilter([]); Monitor For Filter Change to Ensure Dropdown Accurately Displays Filter Change // Watch for element change to indicate filter changes and make change to dropdown if filters don't match displayed filter in dropdown const watchForFilterChange = new MutationObserver((mutations) => { // If element changes mutations.forEach(mu => { // If not class or attribute change return if (mu.type !== "attributes" && mu.attributeName !== "class") return; // Find matching Filter to dropdown let filterToChange = (dashboard.filters.$$filters as Array<DashObject>).filter(element => element.jaql.dim === `[${dim}]`); // If filter values displayed and filter values active don't match each other, set displayed filter to match filter, filter value stays as is filterToChange.forEach((filter) => { if (filter.jaql.filter.members.sort().join(',') !== selectedFilterValues.sort().join(',')) { // Change state of filter values to new filter, sets displayed filter in dropdown in correct one. setSelectedFilterValues(filter.jaql.filter.members); } }); }); }); // Array of element with 'widget-body' class (created by Sisense.js on widgets) for change to check for filter change const widget_body_array = document.querySelectorAll(".widget-body") // Watch 'widget-body' class for filter changed by other means, such as right click select on value widget_body_array.forEach(el => watchForFilterChange.observe(el, { attributes: true })); Component Details Filter - One for each filter, gets dropdown values and other props for dropdown filter component, takes filter dim as prop, parent of Dropdown Filter component Dropdown Filter - Renders individual filter dropdown, renders dropdown of one filter, handles filter changes Clickable Button - Button that calls a function on click, props include text, color and function called Input Number - Sets widgets per row, has up and down button as well as keyboard input, controlled input only accepts numbers, has default value in config file Load Sisense - Loads Sisense, gets URL of server from config file, has load dashboard function, creates elements to load widgets into on dashboard load call Sidebar - Collapsible Sidebar, on click loads dashboard, content of dashboard can be configured by config file App - Parent component, has loading indicator before Sisense has loaded, contains all other components Config Settings DashboardConfig - Array of objects describing dashboards selectable in sidebar DashboardLabel - text to show in expanded sidebar DashboardID - Dashboard ID, get from url of dashboard in native Sisense Icon - Icon to show in sidebar for dashboard DimArray - Values to filter by SisenseUrl - URL of Sisense server initialDashboardCube - Title of initial dashboard to show defaultSidebarCollapsed - Dashboard initial state, collapsed or not defaultSidebarCollapsedMobile - Dashboard initial state, collapsed or not, on mobile collapseSideBarText - Text shown on element that collapses sidebar* hideFilterNativeText - Text to hide native embedded filter useV1 - Use v1 version of Sisense script defaultWidth - initial state of selector for widgets per row saveEdits - Write back to Sisense changes made to filters, and any other persistent changes loadingIndicatorColor - Color of loading indicator loadingIndicatorType - Type of loading indicator, options from ReactLoading sidebarBackgroundColor - Background color of sidebar widgetMargin - Margin of individual widget Available Scripts In the project directory, you can run: npm start Runs the app in the development mode. Open http://localhost:3000 to view it in the browser. The page will reload if you make edits. You will also see any lint errors in the console. npm test Launches the test runner in the interactive watch mode. See the section about running tests for more information. npm run build Builds the app for production to the build folder. It correctly bundles React in production mode and optimizes the build for the best performance. The build is minified and the filenames include the hashes. Your app is ready to be deployed! See the section about deployment for more information. npm run eject Note: this is a one-way operation. Once you eject, you can’t go back! If you aren’t satisfied with the build tool and configuration choices, you can eject at any time. This command will remove the single-build dependency from your project. Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except eject will still work, but they will point to the copied scripts so you can tweak them. At this point, you’re on your own. You don’t have to ever use eject. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However, we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. Download and extract the zip below:5.3KViews5likes0CommentsBuilding with Blox: A Practical Introduction
Introduction to the Blox plugin in Sisense, starting with the most basic fundamentals. This article is intended to empower users to build confidence in the skills with Blox by introducing the core concepts around code structure, Java, HTML, and CSS.3.3KViews4likes1CommentFiltering by column in table widget
To filter any data we need to add the filter on the right panel. Though this is possible only for a desinger and if a viewer wants to filter any other columns in the widget after the result set is displayed, they will need to download the table and filter in csv/excel. It will be really nice to be able to further filter the result set by viewer after the data is processed/displayed by having this option visible on every column header. Currently, clicking the column header only sorts the data, keeping a filter option on every column can allow the user to narrow down the results before downloading the file. Using this option, the viewers can filter any of their columns in table widget without having the need to download and analyze the results outside sisense.226Views4likes1CommentSisenseJS silent login approach
SisenseJS silent login approach Disclaimer: Please note that this blog post contains one possible custom workaround solution for users with similar use cases. We cannot guarantee that the custom code solution described in this post will work in every scenario or with every Sisense software version. As such, we strongly advise users to test solutions in their environment before deploying them to ensure that the solutions proffered function as desired in their environment. For the avoidance of doubt, the content of this blog post is provided to you "as-is" and without warranty of any kind, express, implied, or otherwise, including without limitation any warranty of security and or fitness for a particular purpose. The workaround solution described in this post incorporates custom coding, which is outside the Sisense product development environment and is, therefore, not covered by Sisense warranty and support services. Description: This article describes how to authenticate Sisense users on the parent application without any page refreshes or redirects. Starting from L2023.11.1, Sisense returns Sisense.JS libraries not depending on authentication. SSO flow will work with SSO.newReturnToValidationBehaviour enabled in Admin -> Server & Hardware -> System Management -> Configuration -> {5 clicks on logo} -> Base Configuration, however, it will redirect users back to the parent application causing page refresh which could be not suitable in terms of user experience. Prerequisites: Basic JS knowledge and Sisense.JS embedding sample (for example, from here: https://sisense.dev/guides/embeddingCharts/jsGettingStarted.html#example) Solution: This logic check if we are authenticated in Sisense, if not - it will get the SSO login URL from the response and create an Iframe with that URL as a source. Once Iframe is loaded and the Sisense session is generated - we will remove Iframe from the page and proceed with Sisense.JS logic. Here is an example of a silent login function: const silentLogin = async () => { let isauth = await fetch(url + '/api/auth/isauth', {credentials: 'include'}) let isauthJson = await isauth.json() if (!isauthJson.isAuthenticated && isauthJson.ssoEnabled) { //Check if we are not logged in and SSO is enabled iframesrc=isauthJson.loginUrl + '?return_to=/api/auth/isauth' } var iframe = document.createElement('iframe'); iframe.src=iframesrc ? iframesrc : url + '/api/auth/isauth'; // set SSO login URL if returned iframe.style.display = 'none'; // Hide iframe initially document.body.append(iframe); // Wait until page is loaded iframe.onload = function () { // Page is loaded, now remove the iframe document.body.removeChild(iframe); startSisense() }; } Once we authenticate the user in an Iframe, we can append the Sisense.JS script and once it is loaded on the page - we run further logic: const startSisense = async () => { const sisensejs = document.createElement('script') sisensejs.src=url + '/js/sisense.v1.js' sisensejs.onload = async () => { await renderdash() } document.head.append(sisensejs) } renderdash() in this case is our function where we connect to Sisense and render assets. Here is an example of full HTML code used for the example. Please note that the host and dashboard ID are coming from the local /config endpoint raised on my side, for local testing you need to replace those values manually or point to your configuration file. <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <div id="sisenseApp" style="display: flex; width:100%"> </div> <div id="main" style="display: flex; flex-direction: column; justify-content: center; width: 80%"></div> <div> <div id="filters" style="width: 20%; min-width: 250px; min-height: 300px; height: 100%"></div> </div> <script type="text/javascript"> let app // Please make sure CSRF is disabled and your app domain is added to CORS allowed list const url = config.host //this value is from local config file, please specify host manually here or point to your configuration file const dashboardId = config.defaultDashboardSisenseJS //this value is from local config file, please specify dashboard ID manually here or point to your configuration file let iframesrc='' const silentLogin = async () => { let isauth = await fetch(url + '/api/auth/isauth') let isauthJson = await isauth.json() if (!isauthJson.isAuthenticated && isauthJson.ssoEnabled) { //Check if we are not logged in and SSO is enabled iframesrc=isauthJson.loginUrl + '?return_to=/api/auth/isauth' } var iframe = document.createElement('iframe'); iframe.src=iframesrc ? iframesrc : url + '/api/auth/isauth'; // set SSO login URL if returned iframe.style.display = 'none'; // Hide iframe initially document.body.append(iframe); // Wait until page is loaded iframe.onload = function () { // Page is loaded, now remove the iframe document.body.removeChild(iframe); startSisense() }; } 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 () => { if (window.Sisense) { window.Sisense = {} } const main = document.getElementById("main") if (!window.Sisense.app) { app = await Sisense.connect(url, true) window.Sisense.app = app } const dash = await app.dashboards.load(dashboardId) window.Sisense.widgets = [] const widgetsRaw = await fetch(url + "/api/v1/dashboards/" + dashboardId + "/widgets?fields=oid,title", { method: 'GET', credentials: 'include' }) let widgets = await widgetsRaw.json() if (widgets.length) { widgets.forEach((w) => { window.Sisense.widgets.push(w) const widgetElement = document.createElement('div'); //widgetElement.style.minHeight = '400px' widgetElement.style.width = '400px' widgetElement.style.height = '400px' widgetElement.setAttribute("id", "widget_" + w.oid); let title = document.createElement('p') title.innerText = w.title main.append(title) main.append(widgetElement) dash.widgets.get(w.oid).container = document.getElementById("widget_" + w.oid) }) } dash.renderFilters(document.getElementById("filters")); dash.refresh() } silentLogin() </script> </body> </html> As a result, we can see the full trace of our flow in a Network tab of the browser:1KViews4likes0Comments