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
    • Use Case Gallery
    All PostsDiscussionsBlogsIdeasQuestions
    Leaderboards
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
    Discussions
    • TagsChevronRightIcon
    Embedding
    • Blog banner
      • Embedding AnalyticsChevronRightIcon

      Using ComposeSDK Planning Documents: Guiding an AI Coding Assistant with a Written Planning Document

                                                                       

      Using ComposeSDK Planning Documents: Guiding an AI Coding Assistant with a Written Planning Document From Zero to ComposeSDK describes building a ComposeSDK application one prompt at a time, and that approach scales well. Adding one page, one widget, one filter at a time, each described on its own and confirmed before moving to the next, works for a small dashboard and for a larger one, provided each addition is independent and the person directing the assistant is already thinking through the pieces one at a time. This article describes a different way of working with an AI coding assistant. It is not strictly better than the approach in that article, simply different. Rather than prompting through a ComposeSDK build step by step, the assistant drafts a written plan first. That plan gets revised over a few rounds while nothing has been built yet, and only then does the assistant implement most or all of it in one longer working session. It is a more involved process than the one in From Zero to ComposeSDK, worth using when enough of a project is already decided in advance that writing it down once is less effort than describing it prompt by prompt, not a step every ComposeSDK build needs. Any modern LLM based coding assistant can drive this workflow the same way it drives the one described in From Zero to ComposeSDK . It needs the same two capabilities, editing files in the project folder and running terminal commands. This article mostly describes Claude Code as an example, but nothing about a written planning document is specific to it, and the same steps apply with another agentic editor, a standalone CLI assistant, or a desktop app with terminal access. This article does not cover installing the editor, the assistant, Node, or ComposeSDK itself, and it assumes the setup from From Zero to ComposeSDK is already in place, a scaffolded React project with the ComposeSDK packages installed and a .env file holding the Sisense instance URL and API token. It builds on that setup rather than repeating it. A planning document does not remove the need to review the assistant's work. It moves most of that review earlier, into the document, before the assistant starts writing code against it, rather than after each small step. A plan reviewed carefully before implementation catches a wrong field name or a missing page as a one line edit. The same mistake caught after a long stretch of implementation can require changes in many different places, and costs considerably more time to fix. What goes Into the Planning Document A planning document for a ComposeSDK build works best as a plain markdown file kept in the project, for example plan.md in the project root, rather than something that only exists in the chat history. As a file on disk, it can be opened and edited directly, referenced again in a later session, and it survives a long session's context being condensed in a way that conversation history alone does not. The document does not need a fixed template, but it generally works better when it covers a few things beyond a page and widget list. What the application is for, who uses it, and what habit or decision it supports. Which ComposeSDK flavor the project uses, React, Angular, or Vue, since that decides the package names and component syntax everything else in the plan assumes. The data model involved, referencing the .ts file already generated by the ComposeSDK CLI, or naming the data source if it still needs generating, along with why the fields involved matter to that audience, not just their names. Each page or view and its widgets, specific enough to build from, naming the chart type, the dimensions and measures, and any filters. Interactivity between widgets or pages, such as a filter or a click on one page affecting another. Layout and visual style, to whatever level of detail is already decided, colors, branding, density. Where the Sisense URL and token is and how the assistant checks its own connection without viewing or saving the authentication token, covered in its own section below. A milestone checklist, ordered the way the build should proceed, written as markdown checkboxes so the assistant can mark each one complete as it finishes. Anything explicitly out of scope, so the assistant does not add it unasked while working through a long stretch unsupervised. The checklist carries the most weight during implementation. A long LLM working session eventually has its earlier history summarized, and a agent's summarization keeps only a handful of the most recently read files in full alongside the summary. A checklist file the assistant is instructed to re-open at the start of each milestone stays accurate regardless of how much of the conversation itself has been condensed, because the current state of the build lives in the file rather than in memory of the conversation. Drafting the plan The first cycle is a conversation, not a single prompt. Describing the project and asking for a draft is enough to start. The prompts throughout this article are rough examples of the general tone and type of instruction, not meant to be copied directly. The right wording depends on the specific ComposeSDK application being built. Draft a planning document for a new React ComposeSDK application in this folder, save it as plan.md. It's for the regional sales team, replacing three spreadsheets they currently cross-reference by hand before the weekly pipeline review, built on the Sample ECommerce data model, which mirrors what's in those spreadsheets. Use ComposeSDK's ExecuteQuery function to look at the actual values in a column if that would help, not just the field names in the schema file. The token for authentication is in .env. Cover the purpose, why the data matters to this audience, each page and its filters, any interactivity between pages, a rough visual style, and a milestone checklist. List anything you're unsure of as open questions in its own section rather than guessing. That last instruction matters. An assistant asked to draft a plan will otherwise fill a gap with a guess that sounds reasonable rather than flagging it, and a guess buried in a paragraph of prose is easy to miss on a first read. A dedicated "Open questions" section in the draft is easy to scan and resolve before moving on. Revising the plan across cycles The draft is rarely final on the first pass. Revising it happens either by editing the markdown file directly, or by describing the change and letting the assistant update the file. In plan.md, change the regional breakdown page to a map visualization instead of a bar chart, and add a country filter UI that applies across all pages. Update the milestone checklist to match. Either editing style works, and most planning sessions mix both, a person adjusting a sentence directly while asking the assistant to work out the consequences elsewhere in the document, such as keeping the checklist in sync with a changed page list. As many cycles as needed happen before implementation starts. Nothing has been built yet, so a revision at this stage costs a paragraph, not a refactor. Keeping the connection working while the build runs The plan should note where the Sisense URL and token live, .env , and that the assistant's own read access to it is denied, the setup already covered in From Zero to ComposeSDK. That protection does not need re-explaining here, the plan only needs to point at it. What is worth adding for a longer, less supervised run is two different checks, since they answer different questions. Rotating the token or changing the URL is the only thing that actually requires re-testing whether the assistant can still reach Sisense at all. Reusing the same ComposeSDK CLI command already used to generate the data model file, pointed at a throwaway output path, is a reasonable way to confirm that without the assistant ever seeing the token value, since the script reads .env at run time on its own rather than through a tool call the assistant's file permissions would block. // scripts/check-credentials.mjs // Confirms Sisense credentials still authenticate, without printing // the token. Run with: node --env-file=.env scripts/check-credentials.mjs import { execFileSync } from 'node:child_process'; const url = process.env.VITE_SISENSE_URL; const token = process.env.VITE_SISENSE_TOKEN; try { execFileSync( 'npx', [ '@sisense/sdk-cli', 'get-data-model', '--url', url, '--token', token, '--dataSource', 'Sample ECommerce', '--output', 'scratch/credential-check.ts', ], { stdio: 'ignore' }, ); console.log('Sisense credentials OK'); } catch { console.log('Sisense credential check FAILED'); } This is worth running once after setup, and again only if the plan notes that .env has changed. Running it after an ordinary milestone, like adding a widget, confirms nothing new, since nothing about the credentials changed either. This is for the assistant to run on its own, a person does not need to run it by hand. What the assistant will almost certainly check on its own, once there is a widget to look at, is whether it renders the way the plan describes, and that is a visual check, not a credential check. If the session has a browser automation tool connected, a Playwright MCP server is a common example, the assistant can open the running dev server itself, take a screenshot, and confirm the new chart or page looks right. Asked to verify a milestone and given a way to see the running app, most assistants will reach for exactly this on their own, without needing the mechanism spelled out. Without a connected browser tool, this check still means a person glancing at the running app in a browser, the same as the smoke test described in From Zero to ComposeSDK. If the assistant asks partway through to install a browser automation tool, whether as a yes or no prompt or a plain request, it is usually worth approving. Iterative development goes far better when the assistant can see what its own code produces instead of just describing it. The plan's connection section can state this plainly, without prescribing how. URL and token in .env, read access denied per project settings. Re-run scripts/check-credentials.mjs only if these values change. After each milestone, confirm the change actually renders correctly in the browser. Choosing a permission mode for the implementation run Claude Code, used here as the concrete example, cycles through a few permission modes with Shift+Tab, and the mode in use during implementation determines how often the assistant stops to ask before acting. Manual (the default) asks before every file edit and most shell commands. It suits the planning cycles above, where little is being written yet, but is not practical for a long implementation run, since it interrupts constantly. Accept Edits auto-approves file edits and common filesystem commands, while still asking before other shell commands, such as installing a package or running a build, unless those have already been allow-listed. It is a reasonable default for working through a reviewed plan. Code changes stop interrupting, while a command run for the first time still gets a look. Auto goes further, approving tool calls generally with a background safety check evaluating each action against what was asked, rather than a person reviewing each one. It suits a long stretch of implementation against a plan that has already been reviewed carefully, since there are fewer opportunities to catch a problem as it happens. Bypass Permissions ( --dangerously-skip-permissions at startup) skips prompts almost entirely. It is documented as intended for use inside a container or VM the assistant cannot otherwise damage, not on a developer's own machine. Since the setup this article builds on keeps a live Sisense token in .env on that same machine, this mode is out of scope here. Other LLM's have very similar permission modes. Whichever mode is active, deny rules are checked before any mode grants approval, so the .env protection from From Zero to ComposeSDK stays in effect through Accept Edits and Auto mode as well. Commands already known to be safe and expected by the plan, running the dev server, running tests, regenerating the data model, can be allow-listed directly in .claude/settings.json (or equivalent for your LLM) so they stop prompting even once, while everything else continues to ask. { "permissions": { "allow": [ "Bash(npm run *)", "Bash(npm test *)" ], "deny": [ "Read(./.env)", "Read(./.env.*)" ] } } A Stop hook, configured the same way in settings.json , is a further option worth knowing about. It runs a chosen shell command each time the assistant finishes responding, which can be pointed at the credential check script so it runs automatically after any milestone that touches .env , rather than depending on the assistant remembering the instruction in the plan. The Claude Code hooks documentation is linked below. Other LLM code assistants have similar features. Handing off the finished plan Once the plan reads correctly end to end, implementation itself is one request. Work through plan.md from top to bottom. After finishing each item on the milestone checklist, check it off in the file, confirm the change renders correctly in the browser, and report the result before starting the next item. Re-read plan.md at the start of each milestone rather than relying on memory of earlier parts of this conversation. Stop and ask only when a decision is not covered by the plan. The instruction to re-read the file matters on a long session. It is what keeps the assistant's sense of what is done and what remains accurate even after earlier parts of the conversation have been condensed. Resuming and checking status Because the plan and its checklist live on disk, a new session, or the same session after a break, can pick up where the last one left off. Read plan.md and tell me which milestones are checked off, what remains, and whether any of the finished ones still need a browser check before I can consider them done. This works whether the pause was intentional or the result of the assistant stopping to ask about something the plan did not cover. Example planning document The following is a shortened example of what a finished plan looks like before implementation begins, for a small internal ComposeSDK app. # Regional Sales Pulse *Sisense ComposeSDK Planning Document* ## Purpose Built as a React ComposeSDK application for the regional sales managers who currently pull this picture together from three spreadsheets before the weekly pipeline review. The app should answer, at a glance, whether a region or category is trending up or down. It exists specifically for that meeting, and is meant to make it faster and more informative. ## Data model and what it contains Sample ECommerce (src/models/sample-ecommerce.ts). Revenue and Units are the two figures managers actually watch weekly. Category and Country are the two dimensions they currently cross-reference by hand for trends. Condition (New/Used) does not matter here and should not appear on any page unless someone asks for it later. ## Pages, filters, and interactivity 1. Overview. Revenue and Units by month, column chart. This is the page a manager opens first, so it should load with the current calendar year already selected. 2. Regional Breakdown. Revenue by Country, map visualization. 3. Product Performance. Revenue by Category, ranked bar chart, highest to lowest. Two filters sit at the top of the app and apply across all three pages, a date range defaulting to the last two weeks, and a country selector. Both should be visible without opening a menu. ## Visual style Matches the internal tools intranet look, navy header, white background, no dark mode needed for this audience. Cards with some padding around each chart rather than charts running edge to edge. Nothing more elaborate than that is expected here. ## Connection and verification URL and token in .env, read access denied per project settings. Re-run scripts/check-credentials.mjs only if these values change. After each milestone, confirm the change actually renders correctly in the browser. ## Milestones - [x] Scaffold three page routes with placeholder headers and the shared filters wired to nothing yet - [ ] Overview page with Revenue and Units by month - [ ] Regional Breakdown page with Revenue by Country as a map - [ ] Product Performance page with Category ranked by Revenue - [ ] Shared date range and country filters applied across all three pages - [ ] Navigation between the three pages - [ ] Visual pass matching the style notes above ## Testing Add unit and integration tests where they make sense, and skip them where they don't. Use headless browser screenshots for visual checks, the same way each milestone gets confirmed in the browser. Confirm at least once, early on, that a real query actually returns data, since the credential check script only proves the token authenticates, not that a query returns rows. ## Out of scope No user accounts or role management beyond what Sisense already provides. No PDF export or scheduled email in this version. Condition does not appear anywhere unless a later request asks for it. ## Open questions - Should the country filter support selecting more than one country at once? Left single select for now, since that already matches what the spreadsheets show today. Prompt library Drafting and revising. Draft a planning document for [project description] as a [React/Angular/Vue] ComposeSDK application, saved as plan.md. Cover the purpose, why the data matters, which ComposeSDK flavor it uses, each page and its filters, any interactivity, a rough visual style, and a milestone checklist, with open questions listed separately. Use ExecuteQuery to check the actual values in [columns] before finalizing that page in the plan, not just the field names in the schema file. In plan.md, change [specific details] and update the milestone checklist to match. Review plan.md and flag anything ambiguous enough that you would have to guess during implementation. Connection and verification. Add scripts/check-credentials.mjs as described in plan.md, and note that it only needs to run again if the .env values change. After this milestone, confirm it renders correctly in the browser before checking it off. Settings for a longer run. Add an allow rule to .claude/settings.json (or equivalent for the LLM Code Assistant you or using) for [command], so it stops prompting for that one going forward. Add a Stop hook in settings.json that runs scripts/check-credentials.mjs automatically whenever .env changes. Handoff and resumption. Work through plan.md from top to bottom, checking off each milestone as it's finished, confirming each one in the browser, and re-reading plan.md at the start of the next. Stop only for decisions the plan does not cover. Read plan.md and report which milestones are done, what remains, and which finished ones still need a browser check. Useful links From Zero to ComposeSDK ComposeSDK documentation ComposeSDK ExecuteQuery reference Claude Code permissions documentation Claude Code hooks documentation ComposeSDK Github Monorepo Sisense CSDK Github Skills Examples Sisense MCP Github Server Sisense REST API and authentication documentation A written planning document is worthwhile when a application design and purpose is already decided in enough detail to write down once. A application still being thought out piece by piece is usually still faster to build the direct way, one prompt, one page, one widget at a time.

      Jeremy Friedel
      Jeremy FriedelPosted 3 weeks ago
      0
               
    • Blog banner
      • Embedding AnalyticsChevronRightIcon

      Debugging Web Access Token (WAT) Issues with ComposeSDK

                                                                                                                               

      Debugging Web Access Token (WAT) Issues with ComposeSDK In initial testing, developers embedding Sisense dashboards and widgets with ComposeSDK sometimes find that a Web Access Token (WAT) passed to the SisenseContextProvider component through the wat prop fails to authenticate, and dashboards or widgets do not load. This article describes the steps for isolating the cause, starting with confirming the Sisense license includes WAT at all, then working through the token's own configuration. Confirm the Sisense license includes WAT Before debugging the token itself, confirm that the WAT works directly against Fusion, outside of ComposeSDK. Some Sisense licenses do not include WAT as a feature, and a token generated on a server without WAT licensed will not work in ComposeSDK regardless of how it is configured. WAT is also incompatible with the Sisense Multitenancy feature. Test the token directly against Fusion using the following URL pattern, replacing the placeholders with the organization's Sisense URL, the generated token, and the target dashboard or widget ID: https://mysisense.com/wat/insert_your_generated_token/app/main#/dashboards/dashboard_id https://mysisense.com/wat/insert_your_generated_token/app/main#/dashboards/dashboard_id/widgets/widget_id If this URL returns an error with status code 403 and a message stating that the license is turned off, the Sisense license does not currently include WAT. The organization's Sisense account representative can discuss adding WAT to the agreement. Until WAT is added, see "Use alternative authentication when WAT is not available" below for other options to continue development and testing.

      Status Message Screenshot
      Full documentation on WAT is available on the Using Web Access Tokens page. Validate the WAT's own claims If the license includes WAT, confirm the token payload itself is correctly structured before testing in ComposeSDK. These checks apply whether the token is being tested in Fusion, other forms of embedding, or in ComposeSDK. Confirm the token is valid using the "Test Existing Token" function, described on the Using Web Access Tokens page, in the Sisense Admin panel. This runs structure, logic, and data validation together, so it can catch an invalid "sub" user, theme, or dashboard ID in a single check before working through the items below individually.
      Confirm the WAT works when tested directly against Fusion, using the URL pattern from the license section above, if this has not been tested yet. This confirms the token's claims work outside of ComposeSDK before assuming the problem is in the token itself. Confirm the "sub" claim in the token is a valid, existing user id on the server. The current logged in user's user ID can be retrieved from the browser developer console with: prism.user._id
      Prism User ID Console Command
      Confirm the user id in the "sub" claim has access to the relevant data sources. Confirm any theme id included in the token exists on the server. Theme ids can be checked with the List Themes endpoint in the REST API. As a test, try generating a token with no theme id set. Confirm the token's start and end unix timestamps are correct, and that the current unix time falls between them. The site unixtimestamp.com can be used to check this. If dashboard or widget ids are used, confirm those ids are included in the token. Remove any parameters from the WAT that are not strictly required. The number of required parameters is smaller than what default token generation includes. Confirm the secret (public key) used matches the token configuration referenced by the "kid" in the token's header. A secret from a different token configuration produces the error "Invalid public key." If the payload includes large "prm", "res", "flt", or "acl" claims, confirm their combined character count does not exceed 81,200 characters per token. This limit is rarely reached, but can occur with very large permission or filter lists. Isolate whether the failure is specific to ComposeSDK If the token work's in Fusion and other form's of Sisense embedding and passes all checks, but WAT still fails only when used through ComposeSDK, and not through Fusion, the cause is likely in the ComposeSDK application itself rather than the token. Test with a known good token, manually pasted in as a temporary replacement to any variable based structure, to confirm whether the issue is in this particular token or in the surrounding code. If testing with a dashboard id has not worked, try testing with a ComposeSDK widget defined directly in CSDK code, with no dashboard or widget ID server dependency, to rule out an ID mismatch. Test on a blank localhost page with a minimal ComposeSDK implementation, to rule out interference from other libraries in the application. Use alternative authentication when WAT is not available If the Sisense license does not include WAT, ComposeSDK development and testing can continue using other authentication methods. A viewer role user, or higher, is all that either option requires: A bearer token SSO Organizations interested in adding WAT to their license should contact their Sisense account representative.

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

      Hiding Widgets if a Widget Has No Results [Linux]

                                                                                                               

      Hiding Widgets if a Widget Has No Results [Linux] Introduction A common dashboard design requirement is hiding widgets that have no data. This article includes a widget script that conditionally hides an indicator widget when its primary value is empty or represents an N/A like value. This behavior is commonly requested when dashboard filters or formulas result in no meaningful value and the widget should be visually hidden rather than showing an empty or zero indicator. The article also includes an alternative dashboard-level approach that hides widgets based on filter selections, without inspecting widget query results. This alternative is derived from the linked external blog post . This is applicable to both on-cloud and on-prem Sisense in all recent Sisense versions. Use Case Customers often want indicator widgets to disappear when their calculated value is not meaningful. Common examples include: Filters resulting in no matching data Calculations returning N/A, null, or empty values Conditional metrics that only apply to certain filter selections Rather than showing an empty indicator, this approach hides the widget entirely and restores it automatically when the value becomes valid again. Layout Considerations For best visual results, it is ideal to use one of these two layouts for the indicator widgets that may be hidden: Place the indicator widget in its own dashboard row Place it at the end of a row Hiding a widget via a script does not automatically resize or reflow other widgets on the same row. Step by Step Guide Widget Script to Hide Widget Based on Indicator Value This widget script is applied directly to the indicator widget. It evaluates the widget’s primary value after each render and hides or shows the widget accordingly. Behavior If the primary value is null, empty, or an N/A like string, the widget container is set to display: none. When the value becomes valid and not null again, the widget is restored to visibility and redrawn to ensure correct indicator rendering. A guard variable prevents infinite redraw loops, since a redraw triggers the widget ready event. A debug flag allows optional console logging when needed. Widget Script /** * Hide widget when its primary indicator value is empty or N/A-like. * * Behavior: * - If the value is empty, set the widget container to display "none". * - If the returned value becomes a number or valid value, restore display style to original and redraw * - A redraw triggers "ready" again, so a guard variable prevents a redraw loop. * * Debug: * - Set debug variable = true to enable console logging to track script status. * * For best results use on indicator in own row or at end of row, if other widgets exist on row, empty space will appear in dashboard */ widget.on("ready", function () { function run() { var debug; var suppressNextReady; var hideValues; debug = false; suppressNextReady = false; hideValues = [ "n/a", "#n/a", "na", "none", "null", "undefined" ]; // Function to turn console logging on or off function log(message, data) { if (!debug) { return; } if (data === undefined) { console.log("[hide-empty-indicator] " + message); return; } console.log("[hide-empty-indicator] " + message, data); } // Widget CSS selector function getWidgetElement() { return document.querySelector('widget[widgetid="' + widget.oid + '"]'); } // Get value of primary indicator value function getPrimaryValue() { if ( widget.queryResult && widget.queryResult.value && widget.queryResult.value.data !== undefined ) { return widget.queryResult.value.data; } try { if ( widget.queryResult && widget.queryResult.data && widget.queryResult.data.length ) { return widget.queryResult.data[0][0]; } } catch (e) { log("Value read failed for data[0][0].", e); } if (widget.queryResult && Array.isArray(widget.queryResult)) { if (widget.queryResult.length && widget.queryResult[0].length) { if (widget.queryResult[0][0]) { return widget.queryResult[0][0].Value; } } } return null; } function shouldHide(value) { var text; var normalized; if (value === null || value === undefined) { return true; } text = String(value).trim(); if (!text) { return true; } normalized = text.replace(/\\/g, "/").toLowerCase(); return hideValues.indexOf(normalized) !== -1; } function isElementHidden(element) { if (!element) { return false; } return element.style.display === "none"; } function hideWidget(element) { if (!element) { return; } if (element.style.display === "none") { return; } element.style.display = "none"; log("Hid widget due to empty/N/A-like value."); } function showWidgetAndRedrawIfNeeded(element) { var wasHidden; if (!element) { return; } wasHidden = isElementHidden(element); element.style.display = ""; if (!wasHidden) { log("Widget already visible."); return; } if (typeof widget.redraw !== "function") { log("Widget restored, redraw not available."); return; } suppressNextReady = true; log("Widget restored, triggering redraw."); widget.redraw(); } function applyRule() { var element; var primaryValue; element = getWidgetElement(); if (!element) { log("Widget container element not found."); return; } if (widget.queryResult === undefined) { log("queryResult is not available yet, keeping widget visible."); element.style.display = ""; return; } primaryValue = getPrimaryValue(); log("Primary value evaluated.", primaryValue); if (shouldHide(primaryValue)) { hideWidget(element); return; } showWidgetAndRedrawIfNeeded(element); } function onReady() { if (suppressNextReady) { suppressNextReady = false; log("Ready fired after redraw, applying rule without redraw."); applyRule(); return; } applyRule(); } onReady(); } run(); }); Notes The script uses display: none instead of jQuery hide or show to avoid layout and rendering issues with indicator widgets. Redraw is triggered only when restoring visibility, not when hiding. The script relies only on the widget ready event, which fires again after redraw and filter changes. Dashboard Script to Hide Widgets Based on Filter Selections As an alternative, widgets can be hidden purely based on filter selections, without checking whether the widget returns data. This approach is useful when visibility rules are deterministic based on filters. This method uses a dashboard script and CSS classes to hide widget containers. Example Dashboard Script dashboard.on('filterschanged', function (se, ev) { let filterName = 'Region' //mapping of filter items and widgets to be hidden. //if selected filter item is not available in the list, widgets in 'default' key will be hidden let itemWidgetMapping = { 'Midwest':['6390b5a285a029002e9e2ad6'], 'South': ['6238887ba77683002ea4425b'], 'West':['6390b5a285a029002e9e2ad6', '6238887ba77683002ea4425b'], 'default':[] } selectedFilter = ev.items.find(el=>el.jaql.title == filterName) let selectedItem = 'default' if(selectedFilter && selectedFilter.jaql.filter.members) selectedItem = selectedFilter.jaql.filter.members[0] //unhide all widgets first and then hide widgets based on selected filter $(`widget`).closest('.dashboard-layout-subcell-host').removeClass('dontshowme-parent') if(selectedItem in itemWidgetMapping){ for (const [key, value] of Object.entries(itemWidgetMapping)) { if(key == selectedItem){ itemWidgetMapping[key].forEach(function (item, index) { $(`widget[widgetid="${item}"]`).closest('.dashboard-layout-subcell-host').addClass('dontshowme-parent') }); } } } else{ itemWidgetMapping['default'].forEach(function (item, index) { $(`widget[widgetid="${item}"]`).closest('.dashboard-layout-subcell-host').addClass('dontshowme-parent') }); } }); Choosing the Right Approach Generally the widget script is best suited when: Visibility depends on whether data is returned The indicator value can be empty due to calculations or filters Generally the dashboard script is best suited when: Visibility depends only on filter selections Centralized control over multiple widgets is required These approaches are alternatives and should not be used simultaneously for the same widgets. Conclusion Hiding indicator widgets based on their returned value can potentially improve dashboard clarity and user experience. The widget script approach provides result aware behavior, while the dashboard script approach offers deterministic, filter based control. Both methods are powerful tools for customizing dashboard widget visibility. Two Full Row Indicator Widgets, both visible First Row Indicator Widget is now hidden, by script, due to no data Two Indicators in one row, both visible Second Indicator is now hidden by script, due to no data 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.

      Jeremy Friedel
      Jeremy FriedelPosted 3 months ago
      0
               
    • Discussion
      Micael Santana
      • Knowledge Base Docs
               
      Micael SantanaAdmin
      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.

      3 months 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 3 months 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 3 months ago
      0
               
    • Idea
      • Product Feedback ForumChevronRightIcon
      CORP Support for iFrame Embedding
               
      SamAlbertCSM
      SamAlbertCSMPosted 9 months ago • Last reply 9 months ago
               
      1
               
    • Idea
      • Product Feedback ForumChevronRightIcon
      Dynamic link generation based on values in table/pivot cells in ComposeSDK
                               
      Miroslav_Y
      Miroslav_YPosted 9 months ago
               
      0
               
    • Idea
      • Product Feedback ForumChevronRightIcon
      scripts and plugins working with ComposeSDK
                       
      Martha
      MarthaPosted 10 months ago
               
      0
               
    • Idea
      • Product Feedback ForumChevronRightIcon
      Add indicator for downloading a widget
                       
      ccoelho
      ccoelhoPosted 1 year ago • Last reply 10 months ago
               
      2
               
    …