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
    Code-first Analytics
    • Blog banner
      • APIsChevronRightIcon

      UserReplaceTool - Automating Dashboard Ownership Transfers - Useful for Deleting User Accounts

                                                                                                       

      Automating Dashboard Ownership Transfer in Sisense with UserReplaceTool Managing and deleting user accounts in Sisense can create manual processes when users leave an organization or change roles. A frequent issue is the reassignment of dashboard ownership to prevent losing Sisense dashboards when a given user account is deleted, as deleting a Sisense user will delete all dashboards owned by that user. The UserReplaceTool addresses this task by automating the transfer of dashboard ownership of all dashboards owned by a given user, ensuring continuity and data integrity. Overview UserReplaceTool is a Python-based, API-based Tool solution designed to seamlessly transfer the ownership of dashboards and data models from one user to another in Sisense. This tool simplifies and automates this process, allowing organizations to reassign dashboard ownership without manual processes or the risk of losing dashboards and widgets. All components are accomplished by using Sisense API endpoint requests. Key Features Automated Dashboard Transfer : Reassigns ownership of all dashboards from the current user to a designated replacement user. Data Model Sharing : Ensures that all data models accessible and editable by the previous user are shared with the replacement user. Batch User Processing : Capable of handling multiple user transfers to a replacement user in a single operation, enhancing efficiency. Complete Logging:  All dashboards and datasource transferred are logged both in the console and in a separate log file. Setup Instructions 1. Setting Up the Environment To run the tool within a Python virtual environment , follow these steps: Activate the Python Virtual Environment : source /venv/bin/activate Create a Virtual Environment (if not already present): python3 -m venv .venv Install Dependencies : pip3 install -r requirements.txt For manual installation, including without using a Python virtual environment: pip3 install urllib3 jsonpath_ng pyyaml requests colorama 2. Configuration Edit the settings.yaml file to configure the tool. Key parameters include: Sisense Domain and Port : Specify the URL and port of your Sisense server, which is used for making API requests. API Bearer Token : Provide an admin-level bearer token for API authentication. See the  Sisense API Bearer Token Documentation for instructions on generating and using Bearer Tokens. Users to Replace : List the user IDs to be replaced. User IDs can be retrieved via the Users API or using the console command prism .user._id. Replacement User : Specify the user ID of the new replacement owner of all dashboards. This should typically be an admin level user. Data Model Sharing : Enable or disable the sharing of data models with the Replacement user (True or False), usually True. Dashboard Ownership Transfer : Enable or disable the transfer of dashboard ownership (True or False), usually True. Unlike Dashboards, if a user is deleted, the datasources themselves are not deleted from the server, ownership is automatically transferred to the main System Admin of the Sisense server.  3. Running the Tool Run the tool with the following command:   python3 replaceUser.py Practical Considerations Admin and Network Access : Ensure you have admin-level API access to the Sisense instance. Python Environment : Python3 and pip should be installed on the machine running the tool. All dependencies can be installed using Pip, and a Python virtual environment can be used. Once a Python Virtual Environment folder is set up it can be shared with the Tool and run directly on all systems using the same OS, but it is not cross OS compatible. By automating the transfer of dashboard ownership, UserReplaceTool provides a reliable and efficient solution for managing user transitions in Sisense. This ensures that datasources and dashboards remain accessible and under the control of the appropriate users, maintaining the continuity of Sisense resources. For further customization and configuration details, refer to the attached full Python Tool, which includes a README file and modify the "settings.yaml" file as necessary.    

      Jeremy Friedel
      Jeremy FriedelPosted 2 years ago • Last reply 1 week ago
      4
               
    • Blog banner
      • Widget & Dashboard ScriptsChevronRightIcon

      Programmatically Formatting Bar Chart Widget Value Labels in Sisense (Republished)

                                       

      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 Aug 14, 2026 at 9:34 PM
      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
      • PySisenseChevronRightIcon

      Modifying Sisense Widgets With PySisense

                                                                                                                                       

      Modifying Sisense Widgets With PySisense Overview A chart widget's metadata, including the dimension driving its category axis and the measures driving its values, lives inside the widget's own JSON definition rather than on a separate settings screen. Changing one, through the widget's Edit panel, can be done quickly. Mass modifying the same field across many widgets, or repeating the same change every time a dashboard is rebuilt from a template, is not something the Sisense UI is built to do in bulk. This article describes swap_widget_dimension.py , a Python command-line tool that reads a Sisense chart widget, replaces the field used by one item on a named panel, and writes the result back using the pySisense library. By default it targets the categories panel, a bar chart's category axis, but the same get_widget_by_id and update_widget calls apply to any panel in the widget's metadata.panels structure, including values , so the tool also works for swapping a measure. The worked example throughout is a bar chart with two versions on a dashboard that differ only in their measure: one totals Revenue, the other totals Cost. The script changes only the field of one panel item on one widget per run. It is meant primarily as an example, and can be adapted to cover other widget attributes as needed. The full source is available on GitHub in this repository . About PySisense PySisense is a Python SDK for the Sisense REST API, installed via pip . It provides wrappers around common Sisense operations and handles authentication, session management, and request errors internally. This script uses three methods from pySisense's Dashboard class: get_widget_by_id , which retrieves a widget's full current definition; update_widget , which writes a modified definition back and strips the fields Sisense manages internally ( oid , owner , created , and so on) before sending the request; and, through Dashboard 's access_mgmt attribute, get_my_user , which identifies the Sisense user the API token belongs to. Note: The current release of PySisense on PyPI does not yet include get_widget_by_id , update_widget , or find_widgets_by_type . This functionality is planned for the next PySisense release. The development branch of the PySisense repository already contains it and can be installed directly from there: pip install "git+https://github.com/sisense/pysisense.git@dev" How a chart widget stores its fields A Sisense chart widget's configuration lives under metadata.panels , a list of panel objects. For a bar chart, the panel named categories holds the dimension shown on the category axis, and the panel named values holds the measures. Each panel has an items list, and each item carries a jaql object identifying one field, with table , column , and dim keys ( dim in the form [Table.Column] ), plus a title and other fields, such as datatype or agg , that vary by field type. Panel names and item order can differ by widget type and by how a chart was originally built, so it's worth confirming the structure on the specific widget being changed rather than assuming it. Running the script with --show prints a widget's current panels for exactly this purpose, before anything is changed: python swap_widget_dimension.py --show Prerequisites Python 3.10 or later pySisense (for now, currently the dev branch, this will be merged into the main branch shortly) and PyYAML Network access to the Sisense instance A Sisense API token: Admin > REST API > v1.0 > GET /authentication/tokens/api Only a dashboard's owner can write to its widgets. If the API token's user does not own the dashboard being changed, use a token belonging to the dashboard's owner, or transfer ownership first with pySisense's change_dashboard_owner . Setup pip install "git+https://github.com/sisense/pysisense.git@dev" pyyaml Create a config.yaml with the Sisense instance details, the widget to change, and the field to switch to: sisense: domain: https://your-instance.sisense.com token: YOUR_API_TOKEN_HERE is_ssl: true widget: dashboard_id: YOUR_DASHBOARD_ID widget_id: YOUR_WIDGET_ID dimension: table: Commerce column: Region title: Region Run with --show first to confirm the widget has the panel expected, then continue with the sequence in Usage. Configuration reference sisense.domain , sisense.token , sisense.is_ssl : connection details for the Sisense instance. widget.dashboard_id , widget.widget_id : the widget to change. Both can be overridden for a single run with --dashboard-id and --widget-id without editing config.yaml . widget.admin_access : left unset by default, which auto-detects the right setting (see Troubleshooting below). Set to true or false , or pass --admin-access or --no-admin-access , only to skip that auto-detection. dimension.panel : the metadata.panels entry to change. categories for a bar chart's category axis, values for its measures. Defaults to categories if unset. Can be overridden with --panel . dimension.table , dimension.column , dimension.title : the new field to apply. table and column identify it; title is the label shown on the widget and defaults to column when omitted. All three can be overridden with --table , --column , and --title . Usage Inspect the widget's current panels before changing anything: python swap_widget_dimension.py --show Preview the change without writing it: python swap_widget_dimension.py Apply the change: python swap_widget_dimension.py --apply If the result is not correct, revert to the widget's prior state: python swap_widget_dimension.py --undo Troubleshooting Whether adminAccess is needed depends on ownership, not the Sisense version. get_widget_by_id can fail with a schema validation error mentioning adminAccess , on Sisense instances that reject the parameter outright, or with a permission error, on instances that require it for a widget the API token's user doesn't own. Rather than have the caller guess which applies, the script tries without admin access first, and only retries with admin access if that fails. It also prints a line identifying the token's user and whether that user owns the widget, so the reason either attempt succeeded is visible. --admin-access and --no-admin-access are still available to force one path and skip the auto-detection. Worked example: swapping a measure from Revenue to Cost A dashboard can have two versions of the same bar chart widget that differ only in their values panel, one totaling Commerce.Revenue , the other Commerce.Cost . Pointing the script at the Revenue widget and targeting the values panel swaps it to Cost, keeping its sum aggregation intact: python swap_widget_dimension.py \ --dashboard-id YOUR_DASHBOARD_ID --widget-id YOUR_WIDGET_ID \ --panel values --table Commerce --column Cost --title "Total Cost" \ --apply Running --show against the widget beforehand returns: { "table": "Commerce", "column": "Revenue", "dim": "[Commerce.Revenue]", "datatype": "numeric", "columnTitle": "Revenue", "tableTitle": "Commerce", "agg": "sum", "title": "Total Revenue" } and after --apply : { "table": "Commerce", "column": "Cost", "dim": "[Commerce.Cost]", "datatype": "numeric", "columnTitle": "Cost", "tableTitle": "Commerce", "agg": "sum", "title": "Total Cost" } table , column , dim , columnTitle , tableTitle , and title all changed to Cost; datatype and the sum aggregation in agg carried over unchanged, exactly as build_new_jaql is designed to do. Common use cases Reusing one chart across dashboard variants A bar chart that should show a different measure, or a different category dimension, on each of several dashboards can be built once and adjusted per dashboard with a single command instead of being rebuilt in the widget editor each time. Correcting a field after templating Dashboards built from a template sometimes carry the template's placeholder field. Rather than reopening every affected widget by hand, each one can be pointed at the correct field directly. Working with more than one item on a panel A widget with more than one item on the panel being changed, such as a grouped bar chart with two category dimensions, needs --match-column to identify which one to replace: python swap_widget_dimension.py --apply --match-column Category Full option reference usage: swap_widget_dimension.py [-h] [--config FILE] [--dashboard-id ID] [--widget-id ID] [--panel PANEL] [--table TABLE] [--column COLUMN] [--title TITLE] [--match-column COLUMN] [--show] [--apply] [--undo] [--backup-file FILE] [--admin-access | --no-admin-access] Swap the field used by one item on a Sisense chart widget panel. options: -h, --help show this help message and exit --config FILE Path to config.yaml. --dashboard-id ID Overrides widget.dashboard_id from config.yaml. --widget-id ID Overrides widget.widget_id from config.yaml. --panel PANEL Name of the metadata.panels entry to change, for example categories (a bar chart's category axis) or values (its measures). Overrides dimension.panel from config.yaml. Defaults to 'categories' if neither is set. --table TABLE Overrides dimension.table from config.yaml. --column COLUMN Overrides dimension.column from config.yaml. --title TITLE Overrides dimension.title from config.yaml. --match-column COLUMN Column name of the item to replace within --panel, if it has more than one item. Defaults to the first item. --show Print the widget's current metadata.panels and exit. Use this to confirm panel names and item order before swapping anything. --apply Write the change to Sisense. Without this flag, only a preview is printed. --undo Restore the widget to the state recorded in --backup- file, then exit. --backup-file FILE Where the widget's prior state is saved before a change, and read back from during --undo. --admin-access Always fetch with adminAccess=true, skipping auto- detection. Needed when the API token's user does not own the widget. --no-admin-access Always fetch without adminAccess=true, skipping auto- detection. Needed on Sisense instances that reject adminAccess as an unrecognized query parameter. Use case Using swap_widget_dimension.py instead of manual widget edits offers several advantages: Applies a field change to a widget directly through its JSON definition, without opening the widget editor Works on either a chart's category dimension or its measures, through the same --panel option Provides a way to confirm a widget's panel structure with --show before any change is made Auto-detects whether admin access is needed, rather than requiring the caller to know their token's relationship to the widget in advance Provides a backup of the widget's prior state before every applied change, with --undo to restore it Demonstrates how pySisense's get_widget_by_id and update_widget can be used together to read, modify, and write back any field in a widget's definition, not only its dimension Summary swap_widget_dimension.py gives Sisense administrators and developers a way to change the field used by a chart widget's category axis or its measures from the command line, with a preview step, a way to inspect the widget's actual structure first, auto-detection of whether admin access is needed, and a backup that makes any applied change reversible. The same get_widget_by_id and update_widget pattern used here, fetch the widget, modify the field that needs to change, write it back, applies to other widget fields beyond the ones shown in this example. """swap_widget_dimension.py: swap the field used by one item on a Sisense chart widget panel. Reads a widget with ``Dashboard.get_widget_by_id()``, replaces the field used by one item in a named ``metadata.panels`` entry, such as ``categories`` (a bar chart's category axis) or ``values`` (its measures), and writes the result back with ``Dashboard.update_widget()``. "Dimension" is used loosely here, the way most readers of this script will mean it: the field dropped into that panel, whether it is technically a dimension or a measure. This is a worked example of pySisense's widget read and write functions, not a general purpose widget editor: it changes one field on one panel item on one widget per run, and does not touch anything else on the widget. Note: get_widget_by_id, update_widget, and find_widgets_by_type are not yet in the pySisense release on PyPI. Install from the dev branch to use this script: pip install "git+https://github.com/sisense/pysisense.git@dev" Usage ----- python swap_widget_dimension.py --show # print the widget's panels, change nothing python swap_widget_dimension.py # preview the swap, change nothing python swap_widget_dimension.py --apply # write the change to Sisense python swap_widget_dimension.py --undo # restore from the backup file python swap_widget_dimension.py --panel values --apply # swap a measure instead of the category axis Whether a widget fetch needs adminAccess=true depends on whether the API token's own user already owns the widget. Rather than requiring the caller to know that in advance, fetch_widget() below tries without admin access first and only escalates if that fails. """ from __future__ import annotations import argparse import json import sys from pathlib import Path from typing import Any import yaml from pysisense import Dashboard, SisenseClient SCRIPT_DIR = Path(__file__).parent DEFAULT_CONFIG_PATH = SCRIPT_DIR / "config.yaml" DEFAULT_BACKUP_PATH = SCRIPT_DIR / "widget_backup.json" # The metadata.panels entry this script targets when --panel and # dimension.panel are both left unset: a bar chart's category axis. DEFAULT_PANEL_NAME = "categories" def load_config(path: Path) -> dict[str, Any]: """Load and parse the YAML configuration file. Parameters ---------- path : Path Location of the config.yaml file to read. Returns ------- dict[str, Any] The parsed configuration. Empty dict if the file is empty. Raises ------ SystemExit If no file exists at ``path``. """ if not path.exists(): sys.exit( f"Config file not found: {path}\n" "Copy config.example.yaml to config.yaml and fill in the instance URL, " "token, and widget details." ) with path.open() as config_file: return yaml.safe_load(config_file) or {} def build_client(config: dict[str, Any]) -> SisenseClient: """Construct a SisenseClient from the ``sisense`` section of config.yaml. Parameters ---------- config : dict[str, Any] The full parsed configuration, as returned by :func:`load_config`. Returns ------- SisenseClient A client ready to use with pySisense's Dashboard class. Raises ------ SystemExit If ``sisense.domain`` or ``sisense.token`` is missing. """ sisense_config = config.get("sisense", {}) domain = sisense_config.get("domain", "") token = sisense_config.get("token", "") if not domain or not token: sys.exit("config.yaml must include sisense.domain and sisense.token.") return SisenseClient(domain=domain, token=token, is_ssl=sisense_config.get("is_ssl", True)) def describe_current_user(dashboard: Dashboard) -> dict[str, Any] | None: """Look up the Sisense user the API token belongs to. Used only to print a diagnostic line explaining whether the token's own user already owns the widget being changed, which is what actually determines whether admin access is needed (see :func:`fetch_widget`). A failed lookup here is not fatal to the rest of the script. Parameters ---------- dashboard : Dashboard A pySisense Dashboard instance, whose ``access_mgmt`` attribute exposes ``get_my_user()``. Returns ------- dict[str, Any] | None The logged-in user object (``GET /api/users/loggedin``), or ``None`` if the lookup failed for any reason. """ try: user = dashboard.access_mgmt.get_my_user() except Exception: return None if isinstance(user, dict) and "error" not in user: return user return None def fetch_widget( dashboard: Dashboard, dashboard_id: str, widget_id: str, *, admin_access: bool | None, ) -> tuple[dict[str, Any], bool]: """Fetch a widget, auto detecting whether admin access is required. Whether a fetch needs ``adminAccess=true`` depends on whether the API token's own user already owns the widget, not on the Sisense version. On top of that, some Sisense versions reject ``adminAccess`` outright as an unrecognized query parameter, which looks identical from here to "not the owner". Rather than have the caller guess, this tries the cheaper, no-admin-access request first, and only escalates if that fails. Parameters ---------- dashboard : Dashboard A pySisense Dashboard instance. dashboard_id : str The ``oid`` of the dashboard that contains the widget. widget_id : str The ``oid`` of the widget to fetch. admin_access : bool | None ``True`` or ``False`` forces that mode and skips auto-detection. ``None`` tries without admin access first, then retries with admin access only if the first attempt fails. Returns ------- tuple[dict[str, Any], bool] The fetched widget, and whether admin access was actually used to retrieve it. Raises ------ SystemExit If the widget cannot be fetched in the requested mode(s). """ if admin_access is not None: widget = dashboard.get_widget_by_id(dashboard_id, widget_id, admin_access=admin_access) if "error" in widget: sys.exit(f"Could not fetch widget: {widget['error']}") return widget, admin_access widget = dashboard.get_widget_by_id(dashboard_id, widget_id, admin_access=False) if "error" not in widget: return widget, False error_without_admin_access = widget["error"] widget = dashboard.get_widget_by_id(dashboard_id, widget_id, admin_access=True) if "error" not in widget: return widget, True sys.exit( "Could not fetch widget, with or without admin access.\n" f" Without admin access: {error_without_admin_access}\n" f" With admin access: {widget['error']}" ) def print_fetch_diagnostics(dashboard: Dashboard, widget: dict[str, Any], admin_access_used: bool) -> None: """Print a line explaining whether the token's user owns this widget. Purely informational: makes the reason admin access was, or was not, needed for this particular widget visible, instead of leaving the ``True``/``False`` in ``admin_access_used`` unexplained. Printed to stderr, not stdout, so it never lands in ``--show``'s JSON output and can be piped straight into something like ``jq``. Parameters ---------- dashboard : Dashboard A pySisense Dashboard instance, used to look up the current user. widget : dict[str, Any] The widget returned by :func:`fetch_widget`. admin_access_used : bool Whether the fetch that retrieved ``widget`` used admin access. Returns ------- None """ access_note = "with admin access" if admin_access_used else "without admin access" current_user = describe_current_user(dashboard) if current_user is None: print(f"Fetched widget {access_note} (could not look up the current user for comparison).", file=sys.stderr) return identity = current_user.get("email") or current_user.get("userName") or current_user.get("_id") owns_widget = current_user.get("_id") == widget.get("owner") ownership_note = "owns this widget" if owns_widget else "does not own this widget" print(f"Fetched widget {access_note}. Authenticated as {identity!r}, who {ownership_note}.", file=sys.stderr) def find_panel_item( widget: dict[str, Any], panel_name: str, match_column: str | None, ) -> tuple[list[dict[str, Any]], int]: """Locate the panel item to change. Parameters ---------- widget : dict[str, Any] The full widget object, as returned by ``get_widget_by_id``. panel_name : str The ``metadata.panels`` entry to search, for example ``"categories"`` (a bar chart's category axis) or ``"values"`` (its measures). match_column : str | None If given, selects the item whose current ``jaql.column`` equals it. If ``None``, the first item in the panel is used, which is correct whenever that panel only has one item, such as a simple bar chart's single category dimension or single measure. Returns ------- tuple[list[dict[str, Any]], int] The panel's ``items`` list, and the index within it of the item to change. Raises ------ SystemExit If the widget has no panel named ``panel_name``, the panel has no items, or ``match_column`` does not match any item in it. """ panels = widget.get("metadata", {}).get("panels", []) panel = next((candidate for candidate in panels if candidate.get("name") == panel_name), None) if panel is None or not panel.get("items"): sys.exit(f"No {panel_name!r} panel with items found on this widget. Run --show to inspect its structure.") items = panel["items"] if match_column is None: return items, 0 for index, item in enumerate(items): if item.get("jaql", {}).get("column") == match_column: return items, index sys.exit(f"No item with column {match_column!r} found in the {panel_name!r} panel. Run --show to see the current items.") def build_new_jaql(existing_jaql: dict[str, Any], table: str, column: str, title: str | None) -> dict[str, Any]: """Return a copy of ``existing_jaql`` with its field swapped. ``table``, ``column``, and ``dim`` always change. ``title`` changes to ``title`` or, if that is ``None``, to ``column``. ``columnTitle`` and ``tableTitle`` (Sisense's own display-label mirrors of ``column`` and ``table``, confirmed by inspecting a live widget) are updated to match whenever the existing jaql already had them, so nothing about the field still points at the old one under a different key. ``agg`` and every other key already present (``datatype``, and so on) are carried over unchanged. Preserving ``agg`` in particular is what makes this safe to use on a ``values`` panel item, not only a ``categories`` one: swapping Revenue for Cost keeps the sum aggregation the widget already had, instead of resetting it. Parameters ---------- existing_jaql : dict[str, Any] The jaql object currently on the panel item being replaced. table : str The table of the new field. column : str The column of the new field. title : str | None The label to show on the widget for the new field. Defaults to ``column`` when ``None``. Returns ------- dict[str, Any] A new jaql object with the field swapped. """ new_jaql = dict(existing_jaql) new_jaql["table"] = table new_jaql["column"] = column new_jaql["dim"] = f"[{table}.{column}]" new_jaql["title"] = title or column if "columnTitle" in new_jaql: new_jaql["columnTitle"] = column if "tableTitle" in new_jaql: new_jaql["tableTitle"] = table return new_jaql def write_widget(dashboard: Dashboard, dashboard_id: str, widget_id: str, payload: dict[str, Any]) -> None: """Write ``payload`` back to Sisense via ``Dashboard.update_widget()``. Parameters ---------- dashboard : Dashboard A pySisense Dashboard instance. dashboard_id : str The ``oid`` of the dashboard that contains the widget. widget_id : str The ``oid`` of the widget being written. payload : dict[str, Any] The full widget object to write back. Returns ------- None Raises ------ SystemExit If the API reports a write failure. """ result = dashboard.update_widget(dashboard_id, widget_id, payload) if "error" in result: sys.exit(f"Update failed: {result['error']}") def parse_args() -> argparse.Namespace: """Define and parse this script's command-line arguments. Returns ------- argparse.Namespace The parsed arguments. """ parser = argparse.ArgumentParser(description="Swap the field used by one item on a Sisense chart widget panel.") parser.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), metavar="FILE", help="Path to config.yaml.") parser.add_argument("--dashboard-id", metavar="ID", help="Overrides widget.dashboard_id from config.yaml.") parser.add_argument("--widget-id", metavar="ID", help="Overrides widget.widget_id from config.yaml.") parser.add_argument( "--panel", help="Name of the metadata.panels entry to change, for example categories (a bar " "chart's category axis) or values (its measures). Overrides dimension.panel from " f"config.yaml. Defaults to {DEFAULT_PANEL_NAME!r} if neither is set.", ) parser.add_argument("--table", help="Overrides dimension.table from config.yaml.") parser.add_argument("--column", help="Overrides dimension.column from config.yaml.") parser.add_argument("--title", help="Overrides dimension.title from config.yaml.") parser.add_argument( "--match-column", metavar="COLUMN", help="Column name of the item to replace within --panel, if it has more than one item. " "Defaults to the first item.", ) parser.add_argument( "--show", action="store_true", help="Print the widget's current metadata.panels and exit. Use this to confirm panel " "names and item order before swapping anything.", ) parser.add_argument( "--apply", action="store_true", help="Write the change to Sisense. Without this flag, only a preview is printed.", ) parser.add_argument( "--undo", action="store_true", help="Restore the widget to the state recorded in --backup-file, then exit.", ) parser.add_argument( "--backup-file", default=str(DEFAULT_BACKUP_PATH), metavar="FILE", help="Where the widget's prior state is saved before a change, and read back from during --undo.", ) admin_access_group = parser.add_mutually_exclusive_group() admin_access_group.add_argument( "--admin-access", dest="admin_access", action="store_true", default=None, help="Always fetch with adminAccess=true, skipping auto-detection. Needed when the API " "token's user does not own the widget.", ) admin_access_group.add_argument( "--no-admin-access", dest="admin_access", action="store_false", help="Always fetch without adminAccess=true, skipping auto-detection. Needed on Sisense " "instances that reject adminAccess as an unrecognized query parameter.", ) return parser.parse_args() def main() -> None: """Entry point: parse arguments, then fetch, inspect, swap, or restore a widget's dimension.""" args = parse_args() config = load_config(Path(args.config)) client = build_client(config) dashboard = Dashboard(api_client=client) widget_config = config.get("widget", {}) dashboard_id = args.dashboard_id or widget_config.get("dashboard_id") widget_id = args.widget_id or widget_config.get("widget_id") if not dashboard_id or not widget_id: sys.exit( "dashboard_id and widget_id are required, either in config.yaml under 'widget' " "or via --dashboard-id/--widget-id." ) admin_access = args.admin_access if args.admin_access is not None else widget_config.get("admin_access") if args.undo: backup_path = Path(args.backup_file) if not backup_path.exists(): sys.exit(f"No backup file found at {backup_path}. Nothing to undo.") original_widget = json.loads(backup_path.read_text()) write_widget(dashboard, dashboard_id, widget_id, original_widget) print(f"Widget {widget_id} restored from {backup_path}.") return widget, admin_access_used = fetch_widget(dashboard, dashboard_id, widget_id, admin_access=admin_access) print_fetch_diagnostics(dashboard, widget, admin_access_used) if args.show: print(json.dumps(widget.get("metadata", {}).get("panels", []), indent=2)) return dimension_config = config.get("dimension", {}) panel_name = args.panel or dimension_config.get("panel") or DEFAULT_PANEL_NAME table = args.table or dimension_config.get("table") column = args.column or dimension_config.get("column") title = args.title or dimension_config.get("title") if not table or not column: sys.exit("table and column are required, either in config.yaml under 'dimension' or via --table/--column.") items, index = find_panel_item(widget, panel_name, args.match_column) old_jaql = items[index].get("jaql", {}) new_jaql = build_new_jaql(old_jaql, table, column, title) print(f"\nWidget: {widget.get('title') or widget_id!r} ({widget.get('type')})") print(f" {'Panel:':<15}{panel_name!r}") print(f" {'Current field:':<15}{old_jaql.get('table')}.{old_jaql.get('column')} (title: {old_jaql.get('title')!r})") print(f" {'New field:':<15}{table}.{column} (title: {new_jaql['title']!r})") if not args.apply: print("\n[PREVIEW] No changes made. Re-run with --apply to write this change to Sisense.") return backup_path = Path(args.backup_file) backup_path.write_text(json.dumps(widget, indent=2)) print(f"\nBacked up current widget state to {backup_path}.") items[index]["jaql"] = new_jaql write_widget(dashboard, dashboard_id, widget_id, widget) print(f"Widget {widget_id} updated. To revert: python {Path(__file__).name} --undo --backup-file {backup_path}") if __name__ == "__main__": main()

      Before and After Change Via Tool
      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 1 month ago
      0
               
    • Blog banner
      • Embedding AnalyticsChevronRightIcon

      From Zero to ComposeSDK: Building a Sisense React Application with an AI Coding Assistant

                                                                                                                                       

      From Zero to ComposeSDK: Building a Sisense React Application with an AI Coding Assistant A computer with no development related setup, no code editor application, no programming languages or runtimes such as Node installed, no development tooling of any kind, can create a running ComposeSDK React application in a single development session, connected to a live Sisense instance and ready for continued iteration. The computer can be a laptop, desktop, or server, and the finished app can be served from localhost, a local IP address, or a domain. Almost none of this development and setup has to be done by hand. An AI coding assistant installs the tooling, runs the commands, and writes the code, while the developer describes what should happen and confirms the results along the way. This style of development is sometimes called vibe coding, which refers to working conversationally and letting the AI handle the mechanics. Any modern LLM based coding assistant can drive this workflow. It needs two capabilities, editing code files in the project folder and running terminal commands. This article uses Claude Code in VS Code as the concrete example, but the same steps work in other editors with agentic assistants, in a standalone CLI tool like Claude CLI, or in a desktop app with terminal access like Claude Desktop. The prompts translate directly and are not specific to Claude. Only two steps require manual installation. The first is installing the text editor with its AI assistant, and the second is generating an API token inside the Sisense web interface. Everything else happens through conversation, from installing Node.js to creating the project, configuring credentials, generating the .ts representation of a Sisense data model, and building the widgets, dashboards, and other visualizations themselves. The starting point on the Sisense side is a modern Linux instance with at least one data model, either an ElastiCube or a live model, and permission to generate an API bearer token on it. Prerequisites A modern Linux Sisense instance with at least one data model Permission to generate an API bearer token on that instance A computer with the ability to install programs and CLI access Access to an LLM like Claude, or similar Getting Started An editor and an AI assistant have to be installed before anything can be done conversationally, so the first two steps are done manually. Download and install Visual Studio Code . Open the Extensions view in VS Code, search for the assistant of choice, install it, and sign in. For Claude Code, see the Claude Code documentation . Other agentic assistants with file editing and terminal access work the same way. One behavior is worth knowing about before the first prompt. Agentic assistants ask for approval before running each terminal command, so expect to click Yes regularly during setup, and read each command before approving it. The approval step exists so the developer always knows what is about to run on the machine, and it only provides that protection if the commands are actually read. Most assistants also offer an option to allow certain commands without asking again, which becomes convenient once a pattern has been reviewed a few times. Editing files inside the project folder is a sensible thing to approve universally. Commands that reach outside the project folder, or that install programs system wide, should be reviewed every time and not auto approved. How Granular to Be The sections below proceed step by step, with each prompt doing one focused thing. That structure is deliberate. It makes each step verifiable before the next is added, and it gives the reader a chance to learn what is happening at every stage. It is not a limitation of the LLM. In practice, an agentic assistant handles much larger requests comfortably. Given an empty folder and little else, a single prompt can carry the project most of the way. Set up a new ComposeSDK React application in this folder. Install Node if it is missing, create a React TypeScript Vite project, add the Sisense ComposeSDK packages, and start the dev server. My Sisense instance is at https://example.sisense.com and I will provide an API token in the file where it is needed, tell me where it is. The assistant works through the chain on its own, installing what is missing, flagging where the token goes, and reporting back when the dev server is running. Granular prompts tend to be the better choice while learning. Broad prompts work better once the process is familiar. Most real sessions mix both freely. If any errors are visible, pasting the error message into the LLM chat will often allow the LLM to resolve the issue. Checking the browser developer tools console for error messages to copy can also be helpful. Handing the Environment to the Assistant From this point forward, work happens in the assistant's chat panel. The first task is the development environment itself. To open the Claude VS Code UI, click the orange flower icon in the top right corner when a text file is open in VS Code. Open VS Code in a new empty folder that will hold the project (File, Open Folder, then create or select a directory. Name the folder appropriately, to match the name of the ComposeSDK (CSDK) application). Create an empty text file if the Claude VS Code icon (or the equivalent for the LLM in use) is hidden. Then open the LLM chat panel and ask the assistant to set up the toolchain. Install the latest LTS version of Node on this machine, make sure it is on the path, then verify that node and npm both work from the terminal. The assistant detects the operating system, picks an appropriate install method for that platform, runs it, and confirms the versions. If the path changed during installation, the assistant will usually mention that the terminal or VS Code may need a restart before the change takes effect. Node.js can also be installed manually for the relevant OS from the Node.js website download section , if the LLM struggles to install it via CLI. This first exchange establishes the pattern for everything that follows. State the goal, let the LLM assistant run the commands, confirm the result, move to the next step. Creating the Project With the toolchain verified, the next request creates the application. Create a new React TypeScript app in this folder using Vite, install the dependencies, and start the dev server so I can confirm the default page loads. The assistant plans and sets up the project, installs packages, and starts the Vite dev server, usually at http://localhost:5173 . Open that URL in a browser and confirm the default Vite page renders. Confirming each layer before adding the next is a habit worth keeping. If a later step fails, a verified baseline makes it obvious where the problem was introduced. Installing ComposeSDK ComposeSDK ships as npm packages, so adding it is one request. Add the Sisense ComposeSDK packages to this project: sdk-ui and sdk-data as dependencies, and the SDK CLI as a dev dependency. The three packages serve distinct purposes. @sisense/sdk-ui provides the React components, the charts, dashboards, and filter UI. @sisense/sdk-data provides the query construction layer, including dimensions, measures, and filter factories. @sisense/sdk-cli is a development tool used to generate the TypeScript data model files covered later in this article, and never ships with the application. Full ComposeSDK documentation is available on developer.sisense.com. Connecting to the Sisense Instance Two values connect and authenticate the CSDK application to Sisense, the instance URL and an API bearer token. Generate the token in the Sisense web interface, from the user profile menu under API Token ( Sisense documentation on generating API tokens ). Copy it once generated. The recommended approach keeps the token out of the LLM chat entirely, so the LLM never has access to it. The assistant builds the configuration with the token left blank. Create a .env file with VITE_SISENSE_URL set to https://example.sisense.com and VITE_SISENSE_TOKEN left blank. Add .env to .gitignore, deny your own read access to .env in your permission settings, wrap the app in SisenseContextProvider using those environment variables, and tell me where to paste my token. The assistant creates the .env file with the relevant keys, excludes it from both version control and its own file access, and points to where the token goes. Editing the URL and pasting the token into .env directly in the editor completes the setup. From that point the application can read the bearer token value but the assistant cannot. In Claude Code specifically, the mechanism for blocking its own access is a permission deny rule rather than an ignore file. The rule lives in .claude/settings.json at the project level, or in ~/.claude/settings.json to apply across all projects. { "permissions": { "deny": ["Read(./.env)", "Read(./.env.*)"] } } The /permissions command in Claude Code can also add this rule interactively, saving it to .claude/settings.local.json . Other assistants have their own equivalents, whether ignore files or access controls. Whichever tool is in use, it is worth verifying the block before pasting the real token. Put a dummy value in .env , ask the assistant to read the file, and confirm it refuses. Pasting the token straight into the chat and letting the assistant write the whole file also works, but is not recommended, since it means trusting the chat tool with a credential. Either way, keep .env out of version control and rotate the token if exposure is suspected. Clicking the refresh icon in the Profile bearer token page rotates the token. A token from a viewer role user is all that is required for CSDK, as long as that user has access to every dashboard and data source the application uses. Bearer tokens are also not the only option. ComposeSDK can authenticate with Web Access Tokens (WAT) and SSO as well, which are worth considering beyond initial development ( ComposeSDK authentication documentation ). The resulting setup looks approximately like this in main.tsx , the entry file Vite generates. Wrapping inside App.tsx works just as well, as long as the provider sits above every ComposeSDK component. import { createRoot } from 'react-dom/client'; import { SisenseContextProvider } from '@sisense/sdk-ui'; import App from './App.tsx'; // The provider supplies the connection to every ComposeSDK component below it. // Values come from .env so credentials stay out of source control. createRoot(document.getElementById('root')!).render( <SisenseContextProvider url={import.meta.env.VITE_SISENSE_URL} token={import.meta.env.VITE_SISENSE_TOKEN} > <App /> </SisenseContextProvider> ); Smoke Testing the Connection Before building further, it is worth confirming the application can reach and authenticate with the Sisense instance. There are two reasonable ways to do it. The fastest is DashboardById , which renders an existing Sisense dashboard and requires no data model work in the project. Open any dashboard in Sisense and copy its OID from the browser URL. Render the dashboard with OID 65a1b2c3d4e5f6a7b8c9d0e1 from my Sisense instance on the main page using DashboardById, replacing the default Vite content. The alternative is a minimal chart against a known data model, which previews the widget workflow used in the rest of this article. Either one confirms the same thing, that the URL and token work and the instance is reachable from the browser. A rendered dashboard means the connection is established. A blank or broken one usually traces back to one of these common causes. The token is invalid or belongs to a user without access to the dashboard or data source. Regenerate the token and confirm the user can open the dashboard in Sisense directly. The URL is incorrect, commonly a missing https:// or a trailing path. The value should be the bare instance origin. The browser blocks the request with a CORS error. ComposeSDK typically works without any CORS configuration, but if the console shows a CORS error, an administrator can add http://localhost:5173 to the allowed origins in the Sisense Admin section under Security Settings ( Sisense documentation on CORS configuration ). Browser console errors generally identify which of these applies, and the error output can be pasted straight into the chat for diagnosis. Generating the TypeScript Data Model ComposeSDK includes a built in tool for this step. The SDK CLI installed earlier generates a .ts file that describes the data model, listing its tables, dimensions, and measures as typed TypeScript objects, so widget code references real fields with autocomplete rather than hand typed strings. It works the same way whether the model is an ElastiCube or a live model. Being specific in the prompt matters here. Naming the goal, a .ts file generated by the SDK's own CLI, steers the assistant toward the right tool, there otherwise being the possibility it would try to create or hallucinate its own representation. Use the ComposeSDK CLI (the get-data-model command from @sisense/sdk-cli) to generate a .ts file describing my data model named Sample ECommerce, and save it to src/models. Use the token in .env. Under the hood this runs a command of the following form. # Generates a .ts file describing the data model's tables, dimensions, and measures. # The token and URL are the same values used by the app itself. npx @sisense/sdk-cli get-data-model \ --url https://example.sisense.com \ --token <token> \ --dataSource "Sample ECommerce" \ --output src/models/sample-ecommerce.ts The generated .ts file exports a typed object, conventionally imported as DM , containing every table, dimension, and measure in the model. Everything built from this point on references fields through that object, which means typos become compile errors instead of silently empty charts. Building the First Widget With the .ts model file in place, building the first real chart is a single request. Using the generated Sample ECommerce data model in src/models, add a column chart below the dashboard showing total Revenue by Condition. The assistant writes a component along these lines. import { Chart } from '@sisense/sdk-ui'; import { measureFactory } from '@sisense/sdk-data'; import * as DM from './models/sample-ecommerce'; // A minimal ComposeSDK chart: one category dimension, one aggregated measure. <Chart dataSet={DM.DataSource} chartType="column" dataOptions={{ category: [DM.Commerce.Condition], value: [measureFactory.sum(DM.Commerce.Revenue, 'Total Revenue')], }} /> Once it renders, iteration can be done through continued conversation or manual code editing. The Vite application will auto update with any changes, without requiring manual refreshes. Change it to a bar chart, break it down by Gender, and only include 2024 data. The assistant adds the breakBy entry to the data options and a date filter to the chart, and the result appears on the next hot reload. Describe a change, see it render, refine. That loop is the core of the workflow, and it is the same whether the change is a chart type, a filter, or a layout adjustment. Styling and Structuring the Application The same conversational approach turns the result from a test page into something that looks like an application. Add a professional looking header and footer to the application, including the title [app title], and lay out the dashboard and the chart in cards. What this prompt actually says will vary by user and project. The card layout here is just one option, and the assistant follows whatever visual direction it is given, from minimal to fully branded. One distinction is worth knowing. Application CSS and widget theming are separate layers. Page layout, headers, and footers are ordinary React and CSS. The appearance of the ComposeSDK widgets themselves, including chart colors, fonts, and backgrounds, is controlled through the SDK's ThemeProvider component ( ComposeSDK theming documentation ). Asking for "a dark theme on the charts" versus "a dark background on the page" will, correctly, touch different code. Working with Live Data The ComposeSDK application itself does not typically contain or store data, though exceptions exist, a point that is often misunderstood. Every widget sends a JAQL query to the Sisense instance at render time, so the CSDK application always reflects the current state of the data model. For an ElastiCube that means fresh data appears after each build, and for a live model queries hit the source directly. In neither case does updated data require rebuilding or redeploying the application. The generated .ts model file only needs regeneration when the schema changes, such as new fields, renamed columns, or restructured tables. Data refreshes do not affect it. When the schema does change, regeneration is one request. My data model schema changed. Use the ComposeSDK CLI to regenerate the .ts model file and tell me if any existing widget code references fields that no longer exist. Because the model file is typed, removed fields surface as compile errors, and the assistant can identify and fix the affected components in the same exchange. Prompt Library The prompts below are starting points, not strings that need to be copied exactly. Everything in this article was done conversationally, and describing the goal in natural language as it comes to mind is usually faster than locating a prompt to paste. What makes these prompts work is their specificity, with actual field names, file locations, and a clear definition of when the task is finished. Those qualities carry over to any phrasing, including a single broad prompt that covers several steps at once. Setup and environment: Install the latest LTS version of Node, make sure it is on the path, and verify node and npm from the terminal. Create a React TypeScript Vite app here, install dependencies, and start the dev server. Set up a complete ComposeSDK React app in this folder: install anything missing, create the project, add the SDK packages, and get the dev server running. ComposeSDK installation and connection: Add the ComposeSDK packages: sdk-ui and sdk-data as dependencies and the SDK CLI as a dev dependency. Create a .env with my Sisense URL and a blank token, add it to .gitignore, deny your own read access to it, wire up SisenseContextProvider from those variables, and tell me where to paste my token. The dashboard component shows a network error. Here is the browser console output: [paste]. Diagnose whether this is CORS, the token, the URL, or something else. Data and widgets: Use the ComposeSDK CLI to generate a .ts file for the data model named [name] into src/models. Render the dashboard with OID [oid] using DashboardById. Add a line chart of [measure] by [date dimension] at month granularity, with a member filter on [dimension] limited to [values]. Add breakBy on [dimension] to the existing column chart and move the legend to the bottom. Advanced: Use useExecuteQuery to fetch [measure] grouped by [dimension] and render the result in a plain HTML table instead of an SDK chart component. Use onBeforeRender on the line chart to set the line width to 3 and enable data labels through the underlying Highcharts options. The advanced prompts reference real ComposeSDK extension points. useExecuteQuery runs a query directly and returns rows for custom rendering, and onBeforeRender exposes the underlying Highcharts options object before a chart draws, allowing customization beyond the SDK's own props ( useExecuteQuery reference , chart customization reference ). Useful Links ComposeSDK documentation ComposeSDK quickstart for React Claude Code documentation Sisense REST API and authentication documentation The techniques in this article generalize. Any capability in the ComposeSDK documentation, from dashboard filters to custom widget types, is reachable the same way. Describe it to the assistant with enough specificity, confirm the result in the browser, and refine conversationally.

      Jeremy Friedel
      Jeremy FriedelPosted 3 months ago • Last reply 2 months ago
      3
               
    • 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
               
    • 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 4 months ago
      1
               
    • Discussion
      BenWalkerRH
      • Help and How-To
               
      BenWalkerRH
      Dynamically changing colours on value labels
                               

      Does anyone know if this if possible either at a widget level AND/OR at a dashboard level? I've tried a bunch of scripts from ChatGPT but none that work as intended.  They either amend the colours but dont respect and changes being mad to rendering such as cell size changes and filters applied etc, or the reverse where it respects the rendering but not the colours changing.. 

      11 months agolast reply 10 months ago
      5
               
    • Discussion
      Rafael Ferreira
      • Help and How-To
               
      Rafael Ferreira
      Conditional Format in BloX using an image
                               

      Hi harikm007​ , DRay​ , Liliia_DevX​   I am using BloX to display a conditon format based on a if a value is above 10% or below 10% and then display a green or red arrow I have in my plugins folder to show.  My script goes as follows: { "style": "@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@700;800&display=swap');", "script": "", "title": "", "conditions": [ { "minRange": "-Infinity", "maxRange": 0.10, "image": "/plugins/assets/icons/red_arrow.png", "color": "#D34A4A", "fontSize": "14px", "fontWeight": "600" }, { "minRange": 0.10, "maxRange": "Infinity", "image": "/plugins/assets/icons/green-up-arrow.svg", "color": "#079B65", "fontSize": "14px", "fontWeight": "600" } ], "titleStyle": [ { "display": "none" } ], "showCarousel": false, "body": [ { "type": "ColumnSet", "columns": [ { "type": "Column", "width": "stretch", "style": { "width": "430px", "height": "145px", "box-sizing": "border-box", "padding": "16px" }, "items": [ { "type": "ColumnSet", "columns": [ { "type": "Column", "width": "stretch", "items": [ { "type": "TextBlock", "text": "No shows - last full month", "style": { "font-family": "Inter, sans-serif", "font-size": "16px", "font-weight": "700", "text-align": "left", "color": "#212A31", "margin": "0" } } ] }, { "type": "Column", "width": "auto", "horizontalAlignment": "right", "style": { "text-align": "right", "min-width": "14px" }, "items": [ { "type": "ColumnSet", "style": { "align-items": "center" }, "columns": [ { "type": "Column", "width": "auto", "items": [ { "type": "Image", "url": "{conditions:image}", "altText": "delta", "horizontalAlignment": "right", "style": { "width": "12px", "height": "10px", "margin-right": "6px", "margin-top": "2px" } } ] }, { "type": "Column", "width": "150", "items": [ { "type": "TextBlock", "text": "{panel:# of unique Patient ID}", "style": { "font-family": "Inter, sans-serif", "font-size": "14px", "font-weight": "600", "color": "{conditions:color}", "text-align": "right", "margin": "0" } } ] } ] } ] } ] }, { "type": "TextBlock", "text": "{panel: No Show}", "style": { "margin-top": "16px", "font-family": "Manrope, Inter, sans-serif", "font-size": "28px", "line-height": "32px", "font-weight": "700", "text-align": "left", "color": "#212A31" } }, { "type": "TextBlock", "text": "Avg 10%", "style": { "margin-top": "8px", "font-family": "Inter, sans-serif", "font-size": "12px", "font-weight": "500", "text-align": "left", "color": "#969696" } } ] } ] } ], "actions": [] }  

      10 months agolast reply 10 months ago
      3
               
    • Discussion
      Rafael Ferreira
      • Help and How-To
               
      Rafael Ferreira
      Breaky By Column with two Values: Is it Possible?
                       

      Hello DRay​ Liliia_DevX​ , I am curious if there is a way to display two values with a break by by a category in Sisense? This is a provided example of what I am trying to achieve. 

      11 months agolast reply 10 months ago
      2