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

    Created Apr 15, 2026

    5 members

    138 discussions

    Docs with downloadable Add-ons & Plug-ins

    Add-ons & Plug-Ins
                       
                                       
      • Add-ons & Plug-InsChevronRightIcon

      [Linux] Sisense Upgrade Advisor Plugin

      Introduction  This article outlines the Sisense Upgrade Advisor, a client-side plugin that helps administrators plan a Sisense Linux upgrade. Given a current version and a target version, it returns a validated, step-by-step upgrade path that respects Sisense's upgrade rules, and it links every step to the official release notes. The plugin runs entirely in the browser, inside the Sisense administration interface. There is no backend service or scheduler to run. It reads the instance's own APIs to detect the current environment, and it keeps the official Sisense documentation as the source of truth. Upgrade Rules A Sisense upgrade is often more than a single hop, and the rules that shape the path are spread across the release notes of the versions in between: Jump limit: No more than three quarterly releases per hop. MongoDB gates: Each major bump (6 at L2024.2.0.79, 7 at L2025.2.0.235, 8 at L2025.3) has a prerequisite: a build below the threshold reaches a named intermediate build first. Kubernetes: Each release supports a bounded range, and Kubernetes moves one minor version at a time. Installer: Provisioning moved from RKE1 to RKE2, with a few environment-specific requirements along the way. The Advisor encodes each documented gate explicitly so the full path holds together and each step shows its Kubernetes range, installer, and MongoDB version, links to its release notes, and carries any advisory the notes call out, storage changes, ingress migration, or a kernel requirement. Notes appear only when they apply. Step-by-Step Guide Prerequisites A running instance of Sisense (Linux). Administrator access to the Sisense administration interface. Installation Place the sisense-upgrade-advisor folder under File Manager/plugins. File Manager is located under Admin → App Configuration → File Management. If File Manager is not available, enable it under Admin → App Configuration → Feature Management → File Management. Confirm the plugin is enabled under Admin → Add-ons.

      Refresh the browser. An "Upgrade Advisor" entry appears in the admin menu, beneath the REST API.
      Using the Upgrade Advisor Plugin Select "Upgrade Advisor" from the admin menu. The panel opens with a "Detected environment" banner that summarises the running build, Kubernetes version, container runtime, and operating system. When the running build matches a known release, the Current version is pre-selected for you.
      Pick a Current version and a Target version, then click on Calculate Path. The result is a step-by-step path from the current version to the target. Each stop shows its build, Kubernetes range, installer, and MongoDB version, links to its release notes, and carries any advisory that applies, for example a MongoDB gate, a feature-compatibility check, a storage or ingress change, or a kernel requirement.  The Requirements tab summarises the Kubernetes compatibility matrix, the operating-system support ranges, and hardware sizing. The Checklist tab provides an interactive pre-upgrade and post-upgrade checklist.  Alternative access The Advisor can also be opened directly by URL at; https://<your-sisense-host>/plugins/sisense-upgrade-advisor/app/index.html which is handy for bookmarking or sharing a link. A signed-in Sisense session is required either way. Download The plugin is available to download here Disclaimer This post describes a custom solution for a specific use case. It may not work in all environments or Sisense versions, so we strongly recommend testing it before deploying to production. This plugin is not officially supported, maintained, or validated as part of Sisense releases. If you have questions or need assistance, please leave a comment on this post. Latest Version that the plugin was tested on : 2026.2.0-build.40 This document describes an advisory tool based on publicly available Sisense documentation. Upgrade paths should always be verified against the official documentation and validated in a non-production environment before production.

      Rafael Trevisan
      Rafael TrevisanPosted 1 week ago • Last reply 6 days ago
      1
               
      • Add-ons & Plug-InsChevronRightIcon

      [Linux] Sisense Slack Plugin

      Introduction This community post outlines the implementation of the Sisense Slack Plugin , which automates the delivery of Sisense dashboard notifications to configured Slack channels. The plugin consists of two core components: Frontend Interface: A Sisense dashboard plugin that provides a UI for configuring webhook URLs, custom messages, and notification schedules. Backend Scheduler: A standalone Node.js script ( slack-scheduler.js ) that retrieves dashboard configurations through the Sisense REST API and sends Slack notifications according to the configured schedules. Step-by-Step Guide Prerequisites A running Sisense (Linux) instance. Administrator or Designer-level access to generate a Sisense API token. A system capable of running a scheduled job every minute (for example, cron or an equivalent scheduler). This can be the Sisense host, an external bastion host, or a Kubernetes cluster. A Slack workspace configured with Workflow Webhooks: https://slack.com/help/articles/360035692513-Guide-to-Slack-Workflow-Builder Installation Generate a service API token in Sisense: Navigate to Admin → App Configuration → Feature Management and enable API Token under User Profile . Open My Profile from the user profile menu. Select API Token and copy the generated token.

      Store the API token securely, as it is injected into the execution script at runtime and is never exposed in the UI. Important: Regenerating an API token invalidates the previous token. Any integrations using the old token must be updated. Setting Up the Slack Workflow To ensure messages render correctly in Slack, create the following text variables in the Workflow Builder webhook trigger: dashboardName message dashboardUrl After creating the variables, add a Send a message step and format the message as: New Report from 📊 {} dashboardName {} message Read more: {} dashboardUrl
      The Send a message step can be customized to fit your use case, as it defines the message sent to the Slack channel. Important: Insert the variables using the {} Insert a variable button.
      Installing the Plugin Place the sisense_slack folder under File Manager/plugins . File Manager is located under Admin → App Configuration → File Management . If File Manager is not available, enable it under Admin → App Configuration → Feature Management → File Management . Backend Scheduler Configuration The backend scheduler ( slack-scheduler.js ) must run every minute using a scheduler. Since it communicates with Sisense through the REST API ( /api/v1/dashboards ), it does not need to run on a Sisense application node. Update config.json with your Sisense URL: { "sisenseUrl": "http://localhost:8081" } Ensure that config.json and slack-scheduler.js are located in the same directory: /opt/sisense/storage/plugins/slack_plugin Option A: Standard Linux Execution (Single-Node or Multi-Node) Ensure Node.js is installed. Examples: Ubuntu/Debian sudo apt update && sudo apt install nodejs RHEL/CentOS/Amazon Linux sudo yum install nodejs Single-Node Deployments Create a secure API token file: sudo mkdir -p /etc echo "YOUR_API_TOKEN" | sudo tee /etc/sisense-slack-token chmod 600 /etc/sisense-slack-token Create a log file: sudo touch /var/log/slack-scheduler.log sudo chmod 666 /var/log/slack-scheduler.log Note: Logging is optional. If enabled, configure log rotation (for example, using logrotate ). Edit your crontab: crontab -e Add the following entry: * * * * * node /opt/sisense/storage/plugins/slack_plugin/slack-scheduler.js >> /var/log/slack-scheduler.log 2>&1 Restart the plugins deployment: kubectl -n <Sisense Namespace> rollout restart deployment plugins Multi-Node Deployments Run the scheduler on a single designated machine (for example, a bastion host or one application node) to prevent duplicate executions. Copy slack-scheduler.js and config.json to the target machine. Update config.json so sisenseUrl points to the load balancer. Create the secure token file: echo "YOUR_API_TOKEN" | sudo tee /etc/sisense-slack-token chmod 600 /etc/sisense-slack-token Create the log file: sudo touch /var/log/slack-scheduler.log sudo chmod 666 /var/log/slack-scheduler.log Edit crontab: crontab -e Add: * * * * * node /path/to/slack-scheduler.js >> /var/log/slack-scheduler.log 2>&1 Option B: Kubernetes (EKS / CronJob) For Kubernetes deployments, package the scheduler as a CronJob. Dockerfile FROM node:18-alpine WORKDIR /app COPY slack-scheduler.js config.json ./ CMD ["node", "slack-scheduler.js"] Build and push the image: docker build -t your-registry/sisense-slack-scheduler:1.0 . docker push your-registry/sisense-slack-scheduler:1.0 Deploy the CronJob: apiVersion: batch/v1 kind: CronJob metadata: name: sisense-slack-scheduler spec: schedule: "* * * * *" concurrencyPolicy: Forbid jobTemplate: spec: template: spec: containers: - name: scheduler image: your-registry/sisense-slack-scheduler:1.0 imagePullPolicy: IfNotPresent env: - name: SISENSE_API_TOKEN valueFrom: secretKeyRef: name: sisense-slack-secret key: api-token restartPolicy: OnFailure Create the secret and deploy: kubectl create secret generic sisense-slack-secret --from-literal=api-token=YOUR_API_TOKEN kubectl apply -f cronjob.yaml Sisense UI Configuration To configure notifications for a dashboard: Open the dashboard. Open the dashboard menu (three dots). Select Configure Slack Report .
      Enter the Slack Workflow webhook URL. Provide a custom message. Choose the schedule (Daily, Weekly, or Monthly) and execution time.
      Click Save Configuration . Select Send Now to trigger the webhook immediately. Once configured, your scheduled dashboard reports will be delivered directly to your Slack channels.
      Disclaimer This post describes a custom solution for a specific use case. It may not work in all environments or Sisense versions, so we strongly recommend testing it before deploying to production. This plugin is not officially supported, maintained, or validated as part of Sisense releases. If you have questions or need assistance, please leave a comment on this post. Latest version tested: 2026.1.2 and 2026.1.0-d . Plugin Download link: https://drive.google.com/file/d/1CfpfEPO2IleRe1N-4ID4fuL5pV3OhsEP/view?usp=sharing

      Priyanka Dhuri
      Priyanka DhuriPosted 1 week ago
      0
               
    • Blog banner
      • Add-ons & Plug-InsChevronRightIcon

      Remove Whiskers from Box and Whisker Widgets

                                               

      Download Link: removeWhiskersFromBoxWhiskerPlot.zip Overview This customization removes the whiskers from Box and Whisker Plot widgets by hiding both the horizontal whisker caps and the vertical whisker lines. This can help simplify the visualization when the focus is only on the quartile ranges and median values. The solution is available in two formats: A widget script version for applying the customization to individual widgets A plugin/add-on version for applying the customization to all Box and Whisker widgets globally across dashboards In addition to removing the whiskers, the script/plugin also: Renames the tooltip labels from "Min" and "Max" to user-defined labels such as "Lower Quartile" and "Upper Quartile" Reorders the tooltip values so the median appears between the quartiles instead of at the end This can be useful for: Simplifying Box and Whisker visualizations Aligning terminology with statistical or business standards Making quartile-based visualizations easier for business users to interpret Standardizing tooltip terminology across dashboards Implementation / Installation Instructions 1. Widget Script Version The script removes the whiskers from Box and Whisker widgets. whiskerWidth controls the horizontal line at the end of the whiskers lineWidth controls the vertical lines above and below the boxes Both attributes are set to 0 to completely remove the whiskers from view. The tooltip labels can also be customized by updating the first three variables in the script. Script // Define the new names for the boxes' "Min", "Max", and "Median" labels const newBoxMaxName = "Upper Quartile"; const newBoxMinName = "Lower Quartile"; const newMedianName = "Median"; // Remove whiskers and lines widget.on('processresult', function(se, ev) { // remove horizontal lines at the end of whiskers ev.result.series[0].whiskerWidth = "0"; // remove vertical lines above and below boxes ev.result.series[0].lineWidth = "0"; }); // Remove whiskers information from tooltip and rename boxes' "Min", "Max", and "Median" to user-defined labels widget.on("beforedatapointtooltip", function(w, args){ args.template = "<div class=\"boxplot-tooltip\"><div class=\"boxplot-tooltip-data\"><div class=\"boxplot-tooltip-value-name\"> " + newBoxMaxName + " </div><div class=\"boxplot-tooltip-value\" data-ng-style=\"{color: model.memberColor}\"> {{model.boxMax}} </div></div><div class=\"boxplot-tooltip-data\"><div class=\"boxplot-tooltip-value-name\"> " + newMedianName + " </div><div class=\"boxplot-tooltip-value\" data-ng-style=\"{color: model.memberColor}\"> {{model.boxMedian}} </div></div><div class=\"boxplot-tooltip-data\"><div class=\"boxplot-tooltip-value-name\"> " + newBoxMinName + " </div><div class=\"boxplot-tooltip-value\" data-ng-style=\"{color: model.memberColor}\"> {{model.boxMin}} </div></div><div class=\"highcharts-tooltip-line-separator\"></div></div><div class=\"boxplot-tooltip-category\"><div class=\"boxplot-tooltip-value-category\"> {{model.xAxis}} </div></div></div>"; }); Steps Copy the script above. Open the widget’s script editor from the widget menu. Paste the script into the editor. Modify the quartile/median labels if needed. Save the script. Refresh the widget. 2. Plugin / Add-on Version This version applies the customization globally instead of configuring each widget individually. Installation Steps Download and unzip the plugin zip file . Extract the files directly. Do not create an additional folder level; the plugin files are already organized correctly within the zip file. Upload the removeWhiskersFromBoxWhiskerPlot folder to the Sisense plugins directory ( /opt/sisense/storage/plugins ). This folder can be accessed from Admin tab -> App Configuration -> File Management -> plugins . Check the Add-ons page in the Admin tab under Server & Hardware and confirm the plugin is listed and enabled. To modify the quartile and median labels, open the config.js file inside the plugin folder and update the configuration values (see more details in the Configurations section below), then save the file. Wait approximately 30–60 seconds for the plugin service to restart. Configurations The following configuration values can be customized to control the labels displayed in the tooltip to any preferred naming convention. newBoxMaxName Controls the label displayed for the top edge of the box (labeled "Max" in the default tooltip). Example: "Upper Quartile" newBoxMinName Controls the label displayed for the bottom edge of the box (labeled "Min" in the default tooltip). Example: "Lower Quartile" newMedianName Controls the label displayed for the median value shown inside the box. Example: "Middle Quartile" 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.

      Tri Anthony
      Tri AnthonyPosted 1 month ago
      0
               
      • Add-ons & Plug-InsChevronRightIcon

      RE-POST: Metadata Plugin - Grouping Provider Example (Multiple Translations for Different User Groups)

                       

      This article shows a good way to implement a grouping provider in the Metadata Plugin . If you have already seen the previous article , this is a newer version of it. This solution covers the following use case: when multiple user groups use different system languages, the relevant metadata translation is applied to the user based on their assigned group. In the example below, we will use two different translations: Portuguese and Spanish, to demonstrate the plugin in action. Step 1: Create the Groups This plugin uses groups as its foundation, so you should begin by navigating to: Admin > User Management > Groups > + Add Group Create the groups required for the plugin setup. For example: Portuguese and Spanish. Step 2: Retrieve the Group IDs Go to the REST API section under the Admin tab. Execute the GET /groups API request to obtain the IDs for each group you created. Step 3: Configure the Plugin Now that you have the group IDs, you can begin configuring the plugin. There are a few concepts inside the plugin that you must understand. The configuration is split into two sections: Configurations that apply translations globally using globalDatasourceAliasing Configurations that apply translations at the datasource level using datasourceAliasing For each language you support, you should configure both sections inside run.js . Global Translation: // Global metadata aliases (not tied to one datasource). // Where this appears: // - dashboards: Dashboard names in Sisense UI (home/list/search/header titles). // - folders: Folder names in Sisense UI (left navigation and browse pages). const globalDatasourceAliasing_PT = { // Maps: <original dashboard name> -> <translated dashboard name> dashboards: { "General Usage": "Usos Gerais", }, // Maps: <original folder name> -> <translated folder name> folders: { "General": "Geral", }, }; This configuration defines translations that apply globally across Sisense, including dashboard names, folder names, and other global metadata. Datasource Translation: // Datasource-level metadata aliases. // Where this appears: // - tables: Table names in Data panel/model metadata. // - formulas: Custom formula names used in fields/widgets. // - hierarchies: Hierarchy names in filter panel and field browser. // - titles: Generic metadata titles resolved by Sisense alias engine. // - columns: Column/field names used in Data panel and widget builders. // - widgets: Widget titles shown on dashboards. const datasourceAliasing_PT = { // Maps: <table name> -> <translated table name> tables: { total: "Total", }, // Maps: <formula name> -> <translated formula name> formulas: { Revenue: "Receita", }, // Maps: <hierarchy name> -> <translated hierarchy name> hierarchies: { "Category by Brand and Age Range": "Categoria por marca e faixa etária", }, // Maps: <title key/name> -> <translated title> titles: { "General Usage": "Usos Gerais", }, // Column alias mappings. // Preferred structure per table: // columns: { <tableName>: { <columnName>: <translated column> } } columns: { purchases: { buyer_name: "Nome do Comprador", }, }, // Maps: <widget title> -> <translated widget title> widgets: { "Open Purchases": "Compras em Aberto", }, }; This section controls translations related to tables, formulas, widget titles, hierarchies, columns, and other datasource-specific metadata. The keys must exactly match the names used in your tables, widgets, formulas, and other metadata objects. Group Config: You can define multiple globalDatasourceAliasing and datasourceAliasing configurations inside the config variable. This array contains the mapping between Sisense groups and translation configurations. Here, you must use the group IDs retrieved in Step 2 and assign them to the appropriate translation configuration. Multiple groups can also share the same configuration. // Group-to-language routing configuration. // Where this is used: // - During provider execution, the user's group IDs are matched against `groupIds`. // - If matched, Sisense receives `globalAlias` + `aliasName` and applies translation. const config = [ { // Human-readable label (for maintainers/logging). language: "Portuguese Translation", // Sisense group IDs that should receive this translation set. groupIds: ["69c5488fd868b533ad8b9a43"], globalAlias: globalDatasourceAliasing, aliasName: datasourceAliasing_PT, }, { // Human-readable label (for maintainers/logging). language: "Spanish Translation", // Sisense group IDs that should receive this translation set. groupIds: ["29c5488fd868b533ad239a43"], globalAlias: globalDatasourceAliasing, aliasName: datasourceAliasing_ES, }, ]; Note: If a user belongs to more than one group with different translations configured, the default system language will be used. A notification similar to the example below will also be displayed. Step 4: Install the Plugin Import the Metadata Plugin with your custom configuration into the plugins directory using the File Manager. Once uploaded, enable the plugin and verify that the translations are being applied correctly. Changes Compared to Previous Version Minor improvements in code readability to make the plugin easier to understand. Added fallback error handling. In the previous version, any invalid configuration could stop all translations from working. Now, only the invalid configuration is ignored. Added debug logs to simplify troubleshooting and debugging. The plugin files are attached below for reference: plugin.json : { "name": "metadata", "source": [ "run.6.js" ], "style": [], "folderName": "metadata", "lastUpdate": "2026-05-05T19:49:50.832Z", "isEnabled": true, "version": "2.0.0" } run.6.js : /** * Metadata Translation Plugin (Group-Based) * * Translates Sisense metadata (field titles, formula names) based on the * Sisense group the logged-in user belongs to. * * How it works: * - On every datasource load, Sisense fires 'beforealiascontextinit'. * - This plugin registers two Provider functions (datasource + global) that * inspect the current user's groups and return the correct alias mapping. * - Sisense then replaces all matching metadata labels in widgets and the Data * panel with the aliased values before rendering. * * Grouping Provider (per Sisense docs): * - Each entry in `config` maps one or more Sisense group IDs to an alias object. * - Retrieve group IDs via the Sisense REST API: GET /api/v1/groups * - A user may belong to multiple groups; if more than one group has a distinct * translation configured, the plugin falls back to default language and shows a * dismissable notification informing the user of the conflict. * * Configuration: * - Update the datasource aliasing objects with the field * names exactly as they appear in the ElastiCube (case-insensitive). * - Update `groupIds` in `config` to match the actual group IDs in your instance. * - Add additional language blocks following the same pattern. * - `globalDatasourceAliasing` handles folder/dashboard name aliasing; populate * it if you also need to translate dashboard or folder names. * * References: * - Sisense Docs: https://docs.sisense.com/main/SisenseLinux/translating-sisense-metadata-on-linux.htm * - Community example: https://community.sisense.com/kb/add-ons_and_plug-ins/metadata-plugin---grouping-provider-example-multiple-translations-for-different-/17114 * * Before Implementation: * - Confirm group IDs via GET /api/v1/groups in the Sisense REST API explorer. * - Verify field names match exactly what is stored in the ElastiCube. * - Deploy this file alongside plugin.json into the Sisense plugins directory: * /opt/sisense/storage/plugins/<plugin-folder>/ * or upload via the Sisense File Manager. */ prism.run([ "$q", "$http", ($q, $http) => { const DEBUG_LOGS = true; const log = (step, payload = null, level = "info") => { if (!DEBUG_LOGS && level !== "error") return; const method = level === "error" ? console.error : console.log; if (payload === null || payload === undefined) { method("[Metadata Translation]", `[${level.toUpperCase()}]`, step); return; } method("[Metadata Translation]", `[${level.toUpperCase()}]`, step, payload); }; // Global metadata aliases (not tied to one datasource). // Where this appears: // - dashboards: Dashboard names in Sisense UI (home/list/search/header titles). // - folders: Folder names in Sisense UI (left navigation and browse pages). const globalDatasourceAliasing_PT = { // Maps: <original dashboard name> -> <translated dashboard name> dashboards: { "General Usage": "Usos Gerais", }, // Maps: <original folder name> -> <translated folder name> folders: { "General": "Geral", }, }; const globalDatasourceAliasing_ES = { dashboards: { "General Usage": "Uso General", }, folders: { "General": "Geral", }, }; // Datasource-level metadata aliases. // Where this appears: // - tables: Table names in Data panel/model metadata. // - formulas: Custom formula names used in fields/widgets. // - hierarchies: Hierarchy names in filter panel and field browser. // - titles: Generic metadata titles resolved by Sisense alias engine. // - columns: Column/field names used in Data panel and widget builders. // - widgets: Widget titles shown on dashboards. const datasourceAliasing_PT = { // Maps: <table name> -> <translated table name> tables: { total: "Total", }, // Maps: <formula name> -> <translated formula name> formulas: { Revenue: "Receita", }, // Maps: <hierarchy name> -> <translated hierarchy name> hierarchies: { "Category by Brand and Age Range": "Categoria por marca e faixa etária", }, // Maps: <title key/name> -> <translated title> titles: { "General Usage": "Usos Gerais", }, // Column alias mappings. // Preferred structure per table: // columns: { <tableName>: { <columnName>: <translated column> } } columns: { purchases: { buyer_name: "Nome do Comprador", }, }, // Maps: <widget title> -> <translated widget title> widgets: { "Open Purchases": "Compras em Aberto", }, }; const datasourceAliasing_ES = { // Maps: <table name> -> <translated table name> tables: { total: "Total", }, // Maps: <formula name> -> <translated formula name> formulas: { Revenue: "Ganancia", }, // Maps: <hierarchy name> -> <translated hierarchy name> hierarchies: { "Category by Brand and Age Range": "Categoría por marca y rango de edad", }, // Maps: <title key/name> -> <translated title> titles: { "General Usage": "Uso General", }, // Column alias mappings. // Preferred structure per table: // columns: { <tableName>: { <columnName>: <translated column> } } columns: { purchases: { buyer_name: "Nombre del Comprador", }, }, // Maps: <widget title> -> <translated widget title> widgets: { "Open Purchases": "Compras Abiertas", }, }; // Group-to-language routing configuration. // Where this is used: // - During provider execution, the user's group IDs are matched against `groupIds`. // - If matched, Sisense receives `globalAlias` + `aliasName` and applies translation. const config = [ { // Human-readable label (for maintainers/logging). language: "Portuguese Translation", // Sisense group IDs that should receive this translation set. groupIds: ["69c5488fd868b533ad8b9a43"], globalAlias: globalDatasourceAliasing_PT, aliasName: datasourceAliasing_PT, }, { // Human-readable label (for maintainers/logging). language: "Spanish Translation", // Sisense group IDs that should receive this translation set. groupIds: ["29c6488fd868b533ad8b9a43"], globalAlias: globalDatasourceAliasing_ES, aliasName: datasourceAliasing_ES, }, ]; const isObj = (v) => v && typeof v === "object" && !Array.isArray(v); const cleanMap = (obj) => Object.fromEntries( Object.entries(obj || {}).filter( ([k, v]) => typeof k === "string" && typeof v === "string", ), ); // Normalizes alias payloads before resolving provider callbacks. // - Retains only supported Sisense sections. // - Drops empty sections and invalid key/value pairs. // - Validates nested column mappings per table. const sanitizeAliasing = (input = {}) => { const result = {}; const sections = [ "tables", "formulas", "hierarchies", "titles", "widgets", "dashboards", "folders", ]; for (const key of sections) { if (isObj(input[key])) { const cleaned = cleanMap(input[key]); if (Object.keys(cleaned).length) result[key] = cleaned; } } if (isObj(input.columns)) { const cols = Object.fromEntries( Object.entries(input.columns) .map(([table, colsObj]) => [ table, isObj(colsObj) ? cleanMap(colsObj) : null, ]) .filter(([, v]) => v && Object.keys(v).length), ); if (Object.keys(cols).length) result.columns = cols; } return result; } // Returns the config block that contains the given group ID. // If no group is configured, returns null (plugin falls back to default labels). const getConfigByID = (groupId) => { const matchedConfig = config.find((cfg) => cfg.groupIds.includes(groupId), ); return matchedConfig || null; }; // Attempts to read current user groups from the in-memory prism session. // This is the fastest path and avoids an API call when session data is present. const getGroups = async () => { await $q.resolve(); if (prism && prism.user && prism.user.groupsName) { log("Resolved user groups from prism session", prism.user.groupsName); return prism.user.groupsName; } log("User groups unavailable in prism session; API fallback may be required"); return null; }; // Shows a dismissable warning when a user belongs to multiple translated groups. // In that conflict case the plugin intentionally resolves null (default language). const dispatchNotification = () => { log("Multiple translated groups detected; showing conflict notification"); const notificationDiv = document.createElement("div"); notificationDiv.classList.add("notification_metadata"); notificationDiv.style = ` background: #444f67; padding: 10px; position: absolute; z-index: 999; border-radius: 10px; margin: 3% auto; color: #fafafa; width: 30%; left: 50%; transform: translateX(-50%); `; const notificationText = document.createElement("span"); notificationText.textContent = "You belong to multiple groups. To ensure a consistent user experience, the platform language has been set to the default English version."; notificationDiv.appendChild(notificationText); const dismissButton = document.createElement("button"); dismissButton.textContent = "Dismiss"; dismissButton.style = ` background: none; color: #39a3fa; border: none; cursor: pointer; `; dismissButton.addEventListener("click", (e) => { e.preventDefault(); notificationDiv.style.display = "none"; log("Conflict notification dismissed by user"); }); notificationDiv.appendChild(dismissButton); document.body.appendChild(notificationDiv); }; // Global provider: resolves dashboard/folder aliases. // Steps: // 1) Get user groups (session first, API fallback). // 2) Collect distinct global alias maps configured for those groups. // 3) If more than one map exists, notify and resolve null to avoid ambiguity. // 4) Otherwise resolve the sanitized alias map. const globalDatasourceProvider = async (resolve, reject) => { try { log("globalDatasourceProvider started"); let groups = await getGroups(); // Sometimes the groups are not available in the session, so we need to fetch them from the API. if (!groups) { log("Fetching user groups from /api/users/loggedin"); const response = await $http.get("/api/users/loggedin"); groups = response.data.groupsName; log("Resolved user groups from API", groups); } const globalAlias = groups .map((g) => getConfigByID(g.id)) .filter((cfg) => cfg !== null) .map((cfg) => cfg.globalAlias); const alias = Array.from(new Set(globalAlias)); log("Computed unique global aliases", alias.length); if (alias.length > 1) { dispatchNotification(); log( "Conflicting global aliases found; resolving null to use default language", ); return resolve(null); } log("Resolving sanitized global alias map", alias[0]); return resolve(sanitizeAliasing(alias[0])); } catch (error) { log("globalDatasourceProvider failed", error, "error"); reject(error); } }; // Datasource provider: resolves metadata aliases (tables, columns, formulas, etc.). // Mirrors the global provider conflict behavior: multiple distinct alias maps // trigger a warning and fallback to default labels. const datasourceProvider = async (_, resolve, reject) => { try { log("datasourceProvider started"); const groups = await getGroups(); log("Processing datasource aliases for groups", groups); const aliasNames = groups .map((g) => getConfigByID(g.id)) .filter((cfg) => cfg !== null) .map((cfg) => cfg.aliasName); const alias = Array.from(new Set(aliasNames)); log("Computed unique datasource aliases", alias.length); if (alias.length > 1) { dispatchNotification(); log( "Conflicting datasource aliases found; resolving null to use default language", ); return resolve(null); } log("Resolving sanitized datasource alias map", alias[0]); return resolve(sanitizeAliasing(alias[0])); } catch (error) { log("datasourceProvider failed", error, "error"); reject(error); } }; // Registers providers before Sisense initializes alias context. // `2000` is the provider timeout in milliseconds. prism.on("beforealiascontextinit", function (ev, args) { log("beforealiascontextinit fired; registering alias providers"); args.register(datasourceProvider, globalDatasourceProvider, 2000); }); }, ]); References/Related Content  Metadata Plugin Repository. Sisense Documentation. Previous Post. Sisense User Groups Documentation. Disclaimer : Please note that this blog post contains one possible custom workaround solution for users with similar use cases. We cannot guarantee that the custom code solution described in this post will work in every scenario or with every Sisense software version. As such, we strongly advise users to test solutions in their environment prior to deploying them to ensure that the solutions proffered function as desired in their environment. For the avoidance of doubt, the content of this blog post is provided to you “as-is” and without warranty of any kind, express, implied, or otherwise, including without limitation any warranty of security and or fitness for a particular purpose. The workaround solution described in this post incorporates custom coding, which is outside the Sisense product development environment and is, therefore, not covered by Sisense warranty and support services.

      Micael Santana
      Micael SantanaPosted 1 month ago
      0
               
    • Blog banner
      • Add-ons & Plug-InsChevronRightIcon

      Managing Sisense Plugin States via the REST API

                                                                               

      Managing Sisense Plugin States via the REST API Overview Manually enabling and disabling Sisense plugins through the admin interface can become time consuming, particularly when keeping multiple Sisense server instances in a consistent state, switching active plugin lists quickly for testing, restoring a previous known working plugin list after an upgrade or support session, or iterating on a plugin during development where having only one plugin active significantly reduces build time for any plugin changes to become active. This article describes a Python command line (CLI) tool that manages Sisense plugin states via the Sisense REST API, using a simple configuration file as the config. The tool supports enabling and disabling plugins, isolating (enabling only one and disabling all others) a single plugin for faster development builds, saving and restoring named snapshots of plugin state, and previewing changes before applying them. This tool does not install or uninstall plugins, and does not update plugins or upgrade/downgrade or modify the code within the plugins. The script, configuration file, and dependencies are included in this article. Sisense instances often have many plugins active, with the enabled list changing over time. Common scenarios include: Plugins being left enabled or disabled after support sessions or testing, or enabling or disabling all plugins The need to quickly reach a server plugin state with no plugins enabled, to isolate whether a plugin is causing a potential issue Maintaining a consistent plugin list across multiple Sisense environments or after upgrades Needing to roll back to a previous plugins active state after a change is potentially creating issues (This tool does not rollback plugin versions, only active plugin lists) Performing these operations manually through the Sisense UI admin panel is practical for occasional changes, but becomes inefficient when the changes are frequent, when many plugins are involved, or when the same configuration needs to be applied reliably and repeatably. Description The plugin manager script reads an enable_plugins list from a configuration file and syncs the Sisense instance to match via the REST API. The script: Enables plugins listed in enable_plugins in the configuration file Optionally disables plugins listed in disable_plugins Leaves all other plugins in their current state by default Saves a timestamped snapshot of the current plugin state before applying any changes, providing an automatic rollback point Supports restoring any saved snapshot to return to a previous state A strict mode is also available, which disables any currently enabled plugin that is not listed in enable_plugins , making the instance exactly match the configuration file. The script also supports previewing what would change without making any API calls, and can save named snapshots for later reference or comparison. Plugin name matching Plugin names in the configuration file accept either the short form ( SwitchDimension ) or the full form ( plugin-SwitchDimension ). Matching is case-insensitive. This applies both to the configuration file and to command line arguments. Prerequisites Python 3.7 or later Network access to the Sisense instance (The tool runs remotely and does not need to be installed directly on the server, or SSH access) A Sisense API token, which can be generated from Admin > REST API > v1.0 > GET /authentication/tokens/api Setup Install the required Python packages: pip install requests pyyaml Create a config.yaml and fill in the Sisense instance URL and API token: sisense: url: https://your-sisense-instance.com token: YOUR_API_TOKEN_HERE Add the plugins to enable to the enable_plugins list. See the Configuration reference section for all available settings. The script is then ready to run. Configuration reference sisense: url: https://your-sisense-instance.com token: YOUR_API_TOKEN_HERE # Admin → REST API → v1.0 → GET /authentication/tokens/api bulk: true # set false to update plugins one at a time instead of a single batch request # Plugins to enable. # Accepts short names (SwitchDimension) or full names (plugin-SwitchDimension). # Matching is case-insensitive. # # Default behavior: enable listed plugins; leave every other plugin unchanged. # With --strict: also disable any currently-enabled plugin NOT in this list, # making the instance exactly match this config. enable_plugins: - examplePlugin # Plugins to explicitly disable (optional). # Only needed in the default sync (no --strict), where unlisted plugins are # left alone. Listing a plugin here ensures it stays off and documents the # intentional exclusion. # disable_plugins: # - anotherExamplePlugin enable_plugins : the list of plugins to enable. By default, plugins not on this list are left in their current state. With --strict , any currently enabled plugin not on this list will be disabled. disable_plugins : optional. Plugins to explicitly disable in the default sync without --strict . This is useful for documenting intentional exclusions when strict mode is not being used. bulk : controls whether plugin state changes are sent as a single batch API request or one at a time. Set to false for individual requests, which is slower but can be useful for diagnosing issues with specific plugins. Usage Run the script with no arguments to enable plugins in enable_plugins , disable plugins in disable_plugins , and leave everything else unchanged: python plugin_manager.py Before making any changes, the script saves a timestamped snapshot of the current plugin state. To restore the most recent snapshot: python plugin_manager.py --restore-snapshot To preview what would change without making any API calls: python plugin_manager.py --dry-run Common Use Cases Maintaining a known good plugin baseline Organizations that need to ensure consistent plugin configuration across upgrades, support sessions, or environment changes can maintain an enable_plugins list in the configuration file. Running the script applies the defined configuration to the instance. To also disable any currently enabled plugin not in the list: python plugin_manager.py --strict Testing on a stock Sisense instance Plugin interference is a common source of issues that are hard to reproduce. To quickly disable all plugins and test against a clean instance, clear enable_plugins in the configuration file and run with --strict . The current state is saved automatically before any changes are made, and can be restored when testing is complete: # with enable_plugins cleared in config.yaml python plugin_manager.py --strict # restore when done python plugin_manager.py --restore-snapshot Developing and testing a single plugin When many plugins are installed, Sisense rebuilds all of them whenever any plugin changes. Using --enable-one disables all currently enabled plugins and enables only the specified plugin, which can significantly reduce rebuild time on instances with many plugins: python plugin_manager.py --enable-one MyPlugin Snapshots The script saves snapshots as YAML files to a snapshots/ folder alongside the script. Snapshots capture the full enabled and disabled plugin state at a point in time. Snapshots are saved automatically before any run that changes state. Named snapshots can also be saved and restored explicitly: # save the current state with a specific name python plugin_manager.py --save-snapshot before-upgrade # list all saved snapshots python plugin_manager.py --list-snapshots # restore a specific snapshot python plugin_manager.py --restore-snapshot before-upgrade Including --log-version when saving a snapshot records the version of each plugin at that point in time, which can be useful for tracking plugin versions across upgrades. Full option reference usage: plugin_manager.py [-h] [--config FILE] [--dry-run] [--no-bulk] [--skip-snapshot] [--log-version] [--strict] [--enable-only | --disable-only] [--enable-one PLUGIN | --save-snapshot [NAME] | --list-snapshots | --restore-snapshot [NAME]] Manage Sisense plugin states via the REST API. options: -h, --help show this help message and exit --config FILE Path to config.yaml (default: config.yaml next to this script) --dry-run Preview what would change; make no API calls and save no snapshot --no-bulk Send one API call per plugin instead of a single batch PATCH. Slower but easier to diagnose partial failures. Overrides sisense.bulk in config. --skip-snapshot Do not automatically save a snapshot before this run --log-version Record each plugin's version field (from the API response) in any snapshot saved during this run (manual --save-snapshot or auto saved snapshot) --strict Also disable every currently enabled plugin that is not listed in enable_plugins. Without this flag, only plugins in disable_plugins are disabled; everything else is left alone. Always active for --restore- snapshot. --enable-only Run only the enable step; skip all disables --disable-only Run only the disable step; skip all enables --enable-one PLUGIN Enable exactly one plugin by name and disable all currently enabled others. Accepts the short or full plugin name. --save-snapshot [NAME] Save the current plugin state as a named snapshot. Omit NAME for an auto generated timestamp filename. Combine with --log-version to also record versions for each plugin. --list-snapshots List all saved snapshots with creation date and enabled plugin count --restore-snapshot [NAME] Restore plugin state to exactly match a saved snapshot. Omit NAME to use the most recently created snapshot. Always strict (disables plugins not in snapshot) unless --enable-only. Plugin names accept the short form (SwitchDimension) or the full form (plugin-SwitchDimension) in both CLI args and config.yaml. Default behavior: enable plugins in enable_plugins, disable plugins in disable_plugins, leave everything else unchanged. Use --strict to also disable any enabled plugin not in enable_plugins. A snapshot is saved automatically before each run that modifies state. Use --skip-snapshot to suppress this. Use Case Managing plugins via the REST API and a configuration file offers several advantages over manual administration: Applies plugin active list consistently and repeatably without manual steps Provides automatic rollback through snapshots saved before each change Supports quickly switching between plugin states for testing or development Reduces the time required to restore a known working plugin list after an issue is resolved Gives server administrators a record of plugin state changes over time (done via the tool and saved via snapshots) Summary With this tool in place, administrators and developers can manage Sisense plugin states quickly and reliably from the command line. The configuration file serves as a single source of truth for the intended plugin set, and the snapshot system ensures that any change can be reversed. Whether maintaining a consistent production environment, isolating issues related to plugins, or iterating on plugin development, the tool provides a practical and repeatable approach to plugin state management. The code is not compressed or obfuscated and can be modified as needed or used as a reference for similar tools. Full Python Code: """ Sisense Plugin Manager: manage Sisense plugin states via the REST API. Overview Manages Sisense plugin states via the REST API. Treats plugin configuration as code: define which plugins should be enabled in config.yaml, run the script, and the instance matches, quickly, from anywhere with network access. Before applying any changes, the current plugin state is automatically captured in a timestamped snapshot. If a change needs to be reverted, a single --restore-snapshot command rolls the instance back to exactly how it was. By default only the plugins explicitly listed in config.yaml are enabled or disabled, everything else is left as is. Use --strict to also disable any enabled plugin not in the list, making the instance enabled plugin list exactly match the config. Common use cases Maintain a known good plugin baseline Keep enable_plugins in config.yaml up to date. Run the script after upgrades, support sessions, or any time the instance may have drifted from the expected state. Test against a stock (no plugins) Sisense instance Eliminate plugins as a potential cause of a bug or issue. Clear enable_plugins in config.yaml, run --strict to disable everything, test, then restore when done, the snapshot is saved automatically before changes: # (clear enable_plugins in config.yaml) python plugin_manager.py --strict # ... reproduce the issue on a vanilla instance ... python plugin_manager.py --restore-snapshot # restores most recent Develop and iterate on a single plugin quickly With many plugins installed Sisense rebuilds all of them on each change. --enable-one isolates a single plugin and disables the rest, cutting rebuild time to a fraction on instances with many plugins: python plugin_manager.py --enable-one MyPlugin Roll back any change instantly Every run saves a timestamped snapshot of the state it found before making changes. Use --list-snapshots to find an earlier state and --restore-snapshot to return to it. Usage # Enable listed plugins, disable listed disable_plugins, leave rest unchanged python plugin_manager.py # Strict: also disables any enabled plugin not in enable_plugins python plugin_manager.py --strict # Preview what would change (no API calls, no auto snapshot) python plugin_manager.py --dry-run python plugin_manager.py --strict --dry-run # Run only one half of the sync python plugin_manager.py --enable-only # enable listed; skip all disables python plugin_manager.py --disable-only # disable listed; skip all enables # Enable exactly one plugin and disable every other currently enabled plugin python plugin_manager.py --enable-one SwitchDimension # Snapshots python plugin_manager.py --save-snapshot [NAME] # name is optional python plugin_manager.py --save-snapshot [NAME] --log-version # also record per-plugin versions python plugin_manager.py --list-snapshots python plugin_manager.py --restore-snapshot [NAME] # omit NAME → most recent # Other flags (combinable with any of the above) --no-bulk one API call per plugin instead of a single batch request --skip-snapshot do not save an auto-snapshot before this run --log-version record each plugin's version in any snapshot saved this run --config FILE use a config file other than the default config.yaml Plugin names All plugin names accept either the short form (SwitchDimension) or the full form (plugin-SwitchDimension), both in CLI arguments and in config.yaml. Matching is case insensitive. API token Admin → REST API → v1.0 → GET /authentication/tokens/api Setup pip install requests pyyaml # Python 3.7+ required cp config.yaml.example config.yaml # Edit config.yaml: set sisense.url, sisense.token, and enable_plugins list """ import sys from datetime import datetime, timezone from pathlib import Path from typing import Optional import requests import yaml # Constants SCRIPT_DIR = Path(__file__).parent DEFAULT_CONFIG_PATH = SCRIPT_DIR / "config.yaml" SNAPSHOTS_DIR = SCRIPT_DIR / "snapshots" PLUGIN_PAGE_SIZE = 20 # plugins returned per page by GET /api/v1/plugins # Config helpers def load_config(config_path: Path) -> dict: """Load and return the YAML config file, printing an error and exiting if missing.""" if not config_path.exists(): print(f"Config file not found: {config_path}") print("Copy config.yaml.example to config.yaml and fill in your instance URL and API token.") sys.exit(1) with config_path.open() as file_handle: return yaml.safe_load(file_handle) def make_auth_headers(api_token: str) -> dict: """Return the Authorization + Content-Type headers required by the Sisense REST API.""" return { "Authorization": f"Bearer {api_token}", "Content-Type": "application/json", } # Plugin name normalization def normalize_plugin_name(raw_name: str) -> str: """ Return a canonical form: lowercase with any leading 'plugin-' prefix stripped. Both 'plugin-SwitchDimension' and 'SwitchDimension' normalize to 'switchdimension', so callers do not need to be consistent about the prefix. """ lowered = raw_name.lower() return lowered[len("plugin-"):] if lowered.startswith("plugin-") else lowered def build_name_lookup(plugin_names: list) -> set: """ Build a set that matches a plugin regardless of whether the caller used the 'plugin-' prefix. Each name contributes two entries: the bare normalized form and the 'plugin-<normalized>' form. Use with plugin_matches_lookup() to test whether an API plugin object is covered by a configured list. """ lookup: set = set() for name in plugin_names: normalized = normalize_plugin_name(name) lookup.add(normalized) lookup.add("plugin-" + normalized) return lookup def plugin_matches_lookup(plugin: dict, name_lookup: set) -> bool: """ Return True if either the plugin's folderName or its API name appears in name_lookup (case insensitive, prefix agnostic). """ plugin_identifiers = {plugin["folderName"].lower(), plugin["name"].lower()} return bool(plugin_identifiers & name_lookup) # Plugin list API def fetch_all_plugins(base_url: str, auth_headers: dict) -> list: """ Retrieve the full plugin list from the Sisense instance via paginated GET requests. Prints progress to stdout and exits on HTTP error. """ print("Fetching plugin list...") try: all_plugins = _paginate_plugins(base_url, auth_headers) except requests.HTTPError as http_error: print(f"Failed to fetch plugins: {http_error}") sys.exit(1) print(f" {len(all_plugins)} plugins total\n") return all_plugins def _paginate_plugins(base_url: str, auth_headers: dict) -> list: """Internal: collect every page from GET /api/v1/plugins.""" endpoint = f"{base_url}/api/v1/plugins" all_plugins: list = [] offset = 0 while True: response = requests.get( endpoint, headers=auth_headers, params={"limit": PLUGIN_PAGE_SIZE, "skip": offset}, timeout=30, ) response.raise_for_status() payload = response.json() page = payload.get("plugins", []) all_plugins.extend(page) offset += len(page) total_count = payload.get("count", 0) print(f" Fetched {offset} / {total_count} plugins...") if offset >= total_count or not page: break return all_plugins # Plugin versions def extract_plugin_versions(plugins: list) -> dict: """ Extract the version field from each plugin object in an API response. Returns a dict mapping folderName → version string for every plugin that carries a version field. Plugins with no version field are omitted so the caller can treat an absent key as 'version unknown'. """ versions: dict = {} for plugin in plugins: version_value = plugin.get("version") or plugin.get("pluginVersion") if version_value: versions[plugin["folderName"]] = str(version_value) return versions # Applying changes def _patch_plugins(base_url: str, auth_headers: dict, updates: list) -> None: """Send a PATCH /api/v1/plugins request. Raises requests.HTTPError on failure.""" response = requests.patch( f"{base_url}/api/v1/plugins", headers=auth_headers, json=updates, timeout=60, ) response.raise_for_status() def apply_plugin_updates( base_url: str, auth_headers: dict, updates: list, use_bulk: bool, ) -> tuple: """ Apply a list of enable/disable changes and print the outcome per plugin. use_bulk=True → single PATCH with all updates at once (faster, one round-trip). use_bulk=False → one PATCH per plugin (slower, easier to diagnose partial failures). Returns (success_count, error_count). """ if use_bulk: print(f"Sending bulk request ({len(updates)} plugin(s))...") try: _patch_plugins(base_url, auth_headers, updates) except requests.HTTPError as http_error: print(f" [ERROR] Bulk request failed: {http_error}") sys.exit(1) for update in updates: status_label = "ENABLED" if update["isEnabled"] else "DISABLED" print(f" [{status_label}] {update['folderName']}") return len(updates), 0 success_count = error_count = 0 for update in updates: folder_name = update["folderName"] status_label = "ENABLED" if update["isEnabled"] else "DISABLED" try: _patch_plugins(base_url, auth_headers, [update]) print(f" [{status_label}] {folder_name}") success_count += 1 except requests.HTTPError as http_error: print(f" [ERROR] {folder_name}: {http_error}") error_count += 1 return success_count, error_count # Snapshot I/O def save_snapshot( snapshot_name: str, enabled_folders: list, *, disabled_folders: Optional[list] = None, plugin_versions: Optional[dict] = None, ) -> Path: """ Write a snapshot YAML file to snapshots/<snapshot_name>.yaml. Fields written (in this order): enable_plugins: sorted list of currently enabled folderNames disabled_plugins: sorted list of currently-disabled folderNames (when provided) plugin_versions: {folderName: version} for each plugin (when provided) created: ISO-8601 UTC timestamp Key ordering is preserved (sort_keys=False) so the file reads naturally. """ SNAPSHOTS_DIR.mkdir(exist_ok=True) # created goes last so the plugin lists appear first in the file snapshot_data: dict = {"enable_plugins": sorted(enabled_folders)} if disabled_folders is not None: snapshot_data["disabled_plugins"] = sorted(disabled_folders) if plugin_versions: snapshot_data["plugin_versions"] = plugin_versions snapshot_data["created"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") snapshot_path = SNAPSHOTS_DIR / f"{snapshot_name}.yaml" with snapshot_path.open("w") as file_handle: yaml.dump(snapshot_data, file_handle, sort_keys=False, default_flow_style=False, allow_unicode=True) return snapshot_path def _snapshot_enable_list(snapshot: dict) -> list: """ Return the enabled plugin list from a snapshot, handling both the current 'enable_plugins' key and the legacy 'plugins' key used by older snapshots. """ return snapshot.get("enable_plugins") or snapshot.get("plugins", []) def load_snapshot(snapshot_name: str) -> dict: """Load a snapshot by stem name, exiting with an error if the file is missing.""" snapshot_path = SNAPSHOTS_DIR / f"{snapshot_name}.yaml" if not snapshot_path.exists(): print(f"Snapshot not found: {snapshot_path}") sys.exit(1) with snapshot_path.open() as file_handle: return yaml.safe_load(file_handle) def list_snapshot_files() -> list: """Return all snapshot YAML files in the snapshots directory, sorted by filename.""" if not SNAPSHOTS_DIR.exists(): return [] return sorted(SNAPSHOTS_DIR.glob("*.yaml")) def latest_snapshot_name() -> str: """ Return the stem of the snapshot with the most recent 'created' timestamp. Exits if no snapshots exist. """ snapshot_files = list_snapshot_files() if not snapshot_files: print("No snapshots found.") sys.exit(1) def created_timestamp(path: Path) -> str: with path.open() as file_handle: return yaml.safe_load(file_handle).get("created", "") return max(snapshot_files, key=created_timestamp).stem def auto_save_snapshot(plugins: list, include_version: bool) -> None: """ Save a snapshot from an already fetched plugin list, before applying changes. Accepts the plugin list fetched at the start of the run so no second API call is needed. Called before any changes are applied, giving every run that changes state a rollback point. Failures are caught and reported as a warning so a snapshot error never blocks the intended change. """ snapshot_name = datetime.now().strftime("auto_%Y%m%d_%H%M%S") print(f"\nSaving snapshot '{snapshot_name}' before applying changes...") try: enabled_folders = [p["folderName"] for p in plugins if p.get("isEnabled")] disabled_folders = [p["folderName"] for p in plugins if not p.get("isEnabled")] plugin_versions = extract_plugin_versions(plugins) if include_version else None snapshot_path = save_snapshot( snapshot_name, enabled_folders, disabled_folders=disabled_folders or None, plugin_versions=plugin_versions, ) print(f" Saved to {snapshot_path} ({len(enabled_folders)} enabled / {len(disabled_folders)} disabled)") except Exception as exc: print(f" Warning: auto snapshot failed: {exc}") # Sync computation def compute_plugin_updates( all_plugins: list, enable_names: list, disable_names: list, *, strict: bool, enable_only: bool, disable_only: bool, ) -> tuple: """ Determine which plugins need their state changed. Default behavior (strict=False): - Enable: plugins in enable_names that are currently disabled. - Disable: plugins in disable_names that are currently enabled. - Leave alone: everything else (not mentioned in either list). With strict=True: - Enable: same as default. - Disable: plugins in disable_names, PLUS any currently enabled plugin that does not appear in enable_names. This makes the instance exactly match enable_names. Direction overrides (applied after the above): enable_only=True → clears to_disable entirely. disable_only=True → clears to_enable entirely. Returns: to_enable: sorted folderNames to enable to_disable: sorted folderNames to disable unmatched_names: entries from enable_names that matched no instance plugin """ enable_lookup = build_name_lookup(enable_names) disable_lookup = build_name_lookup(disable_names) # Plugins from the instance that match the enable list matched_enable_plugins = [p for p in all_plugins if plugin_matches_lookup(p, enable_lookup)] enable_folder_set = {plugin["folderName"] for plugin in matched_enable_plugins} # Plugins that should be enabled but currently are not to_enable: list = [] if disable_only else sorted( plugin["folderName"] for plugin in matched_enable_plugins if not plugin.get("isEnabled") ) # Plugins that should be disabled but currently are enabled if enable_only: to_disable: list = [] elif strict: # Strict: disable every enabled plugin not in the enable list to_disable = sorted( plugin["folderName"] for plugin in all_plugins if plugin.get("isEnabled") and plugin["folderName"] not in enable_folder_set ) else: # Soft: only disable what is explicitly listed in disable_plugins to_disable = sorted( plugin["folderName"] for plugin in all_plugins if plugin.get("isEnabled") and plugin_matches_lookup(plugin, disable_lookup) ) # Entries from enable_names that found no matching plugin in the instance matched_normalized_names = ( {normalize_plugin_name(plugin["folderName"]) for plugin in matched_enable_plugins} | {normalize_plugin_name(plugin["name"]) for plugin in matched_enable_plugins} ) unmatched_names = sorted( name for name in enable_names if normalize_plugin_name(name) not in matched_normalized_names ) return to_enable, to_disable, unmatched_names def _build_update_payload(to_enable: list, to_disable: list) -> list: """Combine enable and disable lists into the PATCH payload format.""" return ( [{"folderName": folder_name, "isEnabled": True} for folder_name in to_enable] + [{"folderName": folder_name, "isEnabled": False} for folder_name in to_disable] ) def _execute_sync( base_url: str, auth_headers: dict, to_enable: list, to_disable: list, use_bulk: bool, ) -> int: """ Build the PATCH payload from to_enable / to_disable and apply it. Prints 'Nothing to change.' if both lists are empty. Returns the error count (0 on full success). """ updates = _build_update_payload(to_enable, to_disable) if not updates: print("Nothing to change.") return 0 _, error_count = apply_plugin_updates(base_url, auth_headers, updates, use_bulk) return error_count # Output helpers def _print_update_plan( to_enable: list, to_disable: list, already_correct_count: int, untouched_count: int = 0, ) -> None: """Print a summary of pending changes before they are applied.""" print(f" To enable: {len(to_enable)}") print(f" To disable: {len(to_disable)}") if already_correct_count > 0: print(f" Already set: {already_correct_count}") if untouched_count > 0: print(f" Untouched: {untouched_count} (not listed in config; left as-is)") print() def _print_dry_run_preview(to_enable: list, to_disable: list) -> None: """Print the would-be changes when --dry-run is active.""" if not to_enable and not to_disable: print("Nothing to change.") if to_enable: print("Would enable:") for folder_name in to_enable: print(f" + {folder_name}") if to_disable: print("Would disable:") for folder_name in to_disable: print(f" - {folder_name}") print("\n[DRY RUN] No changes made.") def _report_unmatched_names(unmatched_names: list) -> None: """Warn about configured plugin names that matched nothing in the instance.""" if unmatched_names: print(f"\nNot found in instance ({len(unmatched_names)}):") for name in unmatched_names: print(f" - {name}") # Commands def cmd_sync_to_config( base_url: str, auth_headers: dict, enable_names: list, disable_names: list, *, dry_run: bool, use_bulk: bool, strict: bool, enable_only: bool, disable_only: bool, skip_snapshot: bool, include_version: bool, ) -> None: """ Default command: sync the instance to match config.yaml. Enables plugins in enable_plugins; in strict mode, also disables any currently enabled plugin not in enable_plugins. Without strict, only plugins explicitly in disable_plugins are disabled. """ all_plugins = fetch_all_plugins(base_url, auth_headers) to_enable, to_disable, unmatched_names = compute_plugin_updates( all_plugins, enable_names, disable_names, strict=strict, enable_only=enable_only, disable_only=disable_only, ) enable_lookup = build_name_lookup(enable_names) disable_lookup = build_name_lookup(disable_names) matched_plugins = [p for p in all_plugins if plugin_matches_lookup(p, enable_lookup)] already_correctly_enabled = sum(1 for p in matched_plugins if p.get("isEnabled")) # Untouched: plugins in neither list, only relevant without --strict, where # we deliberately leave them alone untouched_count = 0 if strict or enable_only else sum( 1 for p in all_plugins if not plugin_matches_lookup(p, enable_lookup) and not plugin_matches_lookup(p, disable_lookup) ) # Table of plugins matched by the enable list if matched_plugins: print(f"Matched {len(matched_plugins)} plugin(s) from enable_plugins:\n") print(f" {'Folder name':<45} {'API name':<35} {'Enabled'}") print(f" {'-'*45} {'-'*35} {'-'*7}") for plugin in sorted(matched_plugins, key=lambda p: p["folderName"]): print(f" {plugin['folderName']:<45} {plugin['name']:<35} {plugin['isEnabled']}") print() _print_update_plan(to_enable, to_disable, already_correctly_enabled, untouched_count) if dry_run: _print_dry_run_preview(to_enable, to_disable) _report_unmatched_names(unmatched_names) return if not skip_snapshot and (to_enable or to_disable): auto_save_snapshot(all_plugins, include_version) error_count = _execute_sync(base_url, auth_headers, to_enable, to_disable, use_bulk) print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {error_count}") _report_unmatched_names(unmatched_names) def cmd_enable_single_plugin( base_url: str, auth_headers: dict, plugin_name: str, *, dry_run: bool, use_bulk: bool, skip_snapshot: bool, include_version: bool, ) -> None: """ Enable exactly one plugin and disable every other currently enabled plugin. The target plugin is identified by name (short or full form, case insensitive). This command does not consult enable_plugins or disable_plugins in config. """ target_name_lookup = build_name_lookup([plugin_name]) all_plugins = fetch_all_plugins(base_url, auth_headers) target_plugin = next( (plugin for plugin in all_plugins if plugin_matches_lookup(plugin, target_name_lookup)), None, ) if target_plugin is None: print(f"Plugin not found: {plugin_name!r}") print("Tip: check spelling against the Sisense admin panel, or run --save-snapshot to capture current names.") sys.exit(1) target_folder = target_plugin["folderName"] target_currently_on = target_plugin.get("isEnabled", False) to_enable = [] if target_currently_on else [target_folder] to_disable = sorted( plugin["folderName"] for plugin in all_plugins if plugin.get("isEnabled") and plugin["folderName"] != target_folder ) print(f"Target plugin: {target_folder}") print(f" Currently: {'enabled' if target_currently_on else 'disabled'}\n") _print_update_plan( to_enable, to_disable, already_correct_count=1 if target_currently_on else 0, ) if dry_run: _print_dry_run_preview(to_enable, to_disable) return if not skip_snapshot and (to_enable or to_disable): auto_save_snapshot(all_plugins, include_version) error_count = _execute_sync(base_url, auth_headers, to_enable, to_disable, use_bulk) print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {error_count}") def cmd_save_named_snapshot( base_url: str, auth_headers: dict, snapshot_name: str, include_version: bool, ) -> None: """Save the current plugin state as a named snapshot file.""" all_plugins = fetch_all_plugins(base_url, auth_headers) enabled_folders = [plugin["folderName"] for plugin in all_plugins if plugin.get("isEnabled")] disabled_folders = [plugin["folderName"] for plugin in all_plugins if not plugin.get("isEnabled")] plugin_versions = extract_plugin_versions(all_plugins) if include_version else None snapshot_path = save_snapshot( snapshot_name, enabled_folders, disabled_folders=disabled_folders or None, plugin_versions=plugin_versions, ) print(f"\nSnapshot '{snapshot_name}' saved to {snapshot_path}") print(f" {len(enabled_folders)} enabled / {len(disabled_folders)} disabled plugin(s) captured.") if plugin_versions is not None: print(f" Versions recorded for {len(plugin_versions)} plugin(s).") def cmd_list_snapshots() -> None: """Print a formatted table of all saved snapshots.""" snapshot_files = list_snapshot_files() if not snapshot_files: print("No snapshots found.") return print(f"Snapshots ({len(snapshot_files)}):\n") print(f" {'Name':<32} {'Created':<22} {'Enabled':<9} {'Versions'}") print(f" {'-'*32} {'-'*22} {'-'*9} {'-'*8}") for snapshot_path in snapshot_files: with snapshot_path.open() as file_handle: snapshot_data = yaml.safe_load(file_handle) enabled_count = len(_snapshot_enable_list(snapshot_data)) versions_logged = "yes" if snapshot_data.get("plugin_versions") else "no" print( f" {snapshot_path.stem:<32} " f"{snapshot_data.get('created', 'unknown'):<22} " f"{enabled_count:<9} " f"{versions_logged}" ) def cmd_restore_from_snapshot( base_url: str, auth_headers: dict, snapshot_name: str, *, dry_run: bool, use_bulk: bool, enable_only: bool, disable_only: bool, skip_snapshot: bool, include_version: bool, ) -> None: """ Restore plugin state to exactly match a saved snapshot. Restore is always strict: plugins in the snapshot are enabled and all other currently enabled plugins are disabled, matching the state at snapshot time. Use --enable-only to skip the disable step (only enable what the snapshot lists). """ snapshot = load_snapshot(snapshot_name) snapshot_enable_list = _snapshot_enable_list(snapshot) print(f"Snapshot '{snapshot_name}' ({snapshot.get('created', 'unknown')})") if snapshot.get("disabled_plugins"): print(f" {len(snapshot_enable_list)} enabled / {len(snapshot['disabled_plugins'])} disabled at snapshot time") else: print(f" {len(snapshot_enable_list)} plugin(s) enabled in snapshot") if snapshot.get("plugin_versions"): print(f" Plugin versions recorded: {len(snapshot['plugin_versions'])}") print() all_plugins = fetch_all_plugins(base_url, auth_headers) # Restore is inherently strict: disable anything not in the snapshot # unless the caller asked only to enable (enable_only=True). to_enable, to_disable, unmatched_names = compute_plugin_updates( all_plugins, enable_names=snapshot_enable_list, disable_names=[], strict=not enable_only, enable_only=enable_only, disable_only=disable_only, ) already_correct_count = max( 0, len(snapshot_enable_list) - len(to_enable) - len(unmatched_names) ) _print_update_plan(to_enable, to_disable, already_correct_count) if dry_run: _print_dry_run_preview(to_enable, to_disable) _report_unmatched_names(unmatched_names) return if not skip_snapshot and (to_enable or to_disable): auto_save_snapshot(all_plugins, include_version) error_count = _execute_sync(base_url, auth_headers, to_enable, to_disable, use_bulk) print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {error_count}") _report_unmatched_names(unmatched_names) # Main def main() -> None: import argparse parser = argparse.ArgumentParser( prog="plugin_manager.py", description="Manage Sisense plugin states via the REST API.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Plugin names accept the short form (SwitchDimension) or the full\n" "form (plugin-SwitchDimension) in both CLI args and config.yaml.\n" "\n" "Default behavior: enable plugins in enable_plugins, disable plugins\n" "in disable_plugins, leave everything else unchanged.\n" "Use --strict to also disable any enabled plugin not in enable_plugins.\n" "\n" "A snapshot is saved automatically before each run that\n" "modifies state. Use --skip-snapshot to suppress this." ), ) # Universal flags parser.add_argument( "--config", default=str(DEFAULT_CONFIG_PATH), metavar="FILE", help="Path to config.yaml (default: config.yaml next to this script)", ) parser.add_argument( "--dry-run", action="store_true", help="Preview what would change; make no API calls and save no snapshot", ) parser.add_argument( "--no-bulk", action="store_true", help=( "Send one API call per plugin instead of a single batch PATCH. " "Slower but easier to diagnose partial failures. " "Overrides sisense.bulk in config." ), ) parser.add_argument( "--skip-snapshot", action="store_true", help="Do not automatically save a snapshot before this run", ) parser.add_argument( "--log-version", action="store_true", help=( "Record each plugin's version field (from the API response) in any " "snapshot saved during this run (manual --save-snapshot or auto saved snapshot)" ), ) parser.add_argument( "--strict", action="store_true", help=( "Also disable every currently enabled plugin that is not listed in " "enable_plugins. Without this flag, only plugins in disable_plugins are " "disabled; everything else is left alone. " "Always active for --restore-snapshot." ), ) # Direction flags (mutually exclusive) direction_group = parser.add_mutually_exclusive_group() direction_group.add_argument( "--enable-only", action="store_true", help="Run only the enable step; skip all disables", ) direction_group.add_argument( "--disable-only", action="store_true", help="Run only the disable step; skip all enables", ) # Mode (mutually exclusive) mode_group = parser.add_mutually_exclusive_group() mode_group.add_argument( "--enable-one", metavar="PLUGIN", help=( "Enable exactly one plugin by name and disable all currently enabled " "others. Accepts the short or full plugin name." ), ) mode_group.add_argument( "--save-snapshot", metavar="NAME", nargs="?", const="__auto__", help=( "Save the current plugin state as a named snapshot. " "Omit NAME for an auto generated timestamp filename. " "Combine with --log-version to also record versions for each plugin." ), ) mode_group.add_argument( "--list-snapshots", action="store_true", help="List all saved snapshots with creation date and enabled plugin count", ) mode_group.add_argument( "--restore-snapshot", metavar="NAME", nargs="?", const="__latest__", help=( "Restore plugin state to exactly match a saved snapshot. " "Omit NAME to use the most recently created snapshot. " "Always strict (disables plugins not in snapshot) unless --enable-only." ), ) args = parser.parse_args() # --list-snapshots reads local files only; no config or API connection needed if args.list_snapshots: cmd_list_snapshots() return # Load config and connect config = load_config(Path(args.config)) sisense_config = config.get("sisense", {}) base_url = sisense_config.get("url", "").rstrip("/") api_token = sisense_config.get("token", "") if not base_url or not api_token: print("config.yaml must include sisense.url and sisense.token.") sys.exit(1) auth_headers = make_auth_headers(api_token) use_bulk = sisense_config.get("bulk", True) and not args.no_bulk # Support both 'enable_plugins' (current key) and 'plugins' (legacy key) enable_names = config.get("enable_plugins") or config.get("plugins", []) disable_names = config.get("disable_plugins", []) # Dispatch if args.enable_one: cmd_enable_single_plugin( base_url, auth_headers, args.enable_one, dry_run=args.dry_run, use_bulk=use_bulk, skip_snapshot=args.skip_snapshot, include_version=args.log_version, ) elif args.save_snapshot is not None: snapshot_name = ( datetime.now().strftime("snapshot_%Y%m%d_%H%M%S") if args.save_snapshot == "__auto__" else args.save_snapshot ) cmd_save_named_snapshot( base_url, auth_headers, snapshot_name, args.log_version, ) elif args.restore_snapshot is not None: snapshot_name = ( latest_snapshot_name() if args.restore_snapshot == "__latest__" else args.restore_snapshot ) cmd_restore_from_snapshot( base_url, auth_headers, snapshot_name, dry_run=args.dry_run, use_bulk=use_bulk, enable_only=args.enable_only, disable_only=args.disable_only, skip_snapshot=args.skip_snapshot, include_version=args.log_version, ) else: # Default: sync instance state to config if not enable_names and not disable_names: print("config.yaml must include at least one entry in 'enable_plugins'.") sys.exit(1) cmd_sync_to_config( base_url, auth_headers, enable_names, disable_names, dry_run=args.dry_run, use_bulk=use_bulk, strict=args.strict, enable_only=args.enable_only, disable_only=args.disable_only, skip_snapshot=args.skip_snapshot, include_version=args.log_version, ) if __name__ == "__main__": main()

      Jeremy Friedel
      Jeremy FriedelPosted 1 month ago • Last reply 1 month ago
      1
               
    • Blog banner
      • Add-ons & Plug-InsChevronRightIcon

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

                                                       

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

      Jeremy Friedel
      Jeremy FriedelPosted 2 years ago • Last reply 2 months ago
      1
               
      • Add-ons & Plug-InsChevronRightIcon

      How to use External Libraries in Sisense Plugins [LINUX]

      Step-by-Step Guide:  Step 1: Understand how Sisense plugins load assets Sisense plugins are loaded into the application at runtime and can include JavaScript, CSS, and static assets. External libraries can run alongside the plugin and may be imported from plugin files or loaded from a CDN, depending on security and deployment requirements. Step 2: Decide how to include the external library There are two supported approaches: Local bundling (recommended) Download the library and store it inside the plugin folder. This avoids dependency on external networks, improves version stability, and provides more control during development. CDN loading Load the library dynamically from a public CDN. This is faster to set up but depends on network availability and security policies such as CORS. You also need to choose a trusted CDN or host one yourself. Local option: Add the library to your plugin folder To use the local option, you must first understand that all files included in the plugin manifest are loaded immediately with the plugin. By default, many libraries expose themselves on the global window object, which allows them to be used globally. For example: $(document).ready() // <-- $ is an instance of jQuery that exists on window, so it can be used everywhere. When using a library inside a plugin, it is strongly recommended to attach it to the prism object, which is a globally reserved object instantiated by Sisense. For example: prism.fuseSparkline = <Fuse.js min code> By doing this, you create a namespace inside prism that allows the library to be reused across the application, especially within the plugin. We use fuseSparkline because we can be confident that no other object key is using it. To properly define this namespace, you need to include the library in the plugin manifest: { "source": [ "lib/fuse.min.js", // <-- The Fuse.js library code "main.js" ] } Load using scripts When using a script-based approach to load the library, the library file is not defined as a namespace by default, so you should copy the full min.js code. With this approach, the loading behavior is similar to using an HTML <script> tag. A basic loader function looks like this: function loadScript(src) { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } // Load the library before using it loadScript('/plugins/<plugin-name>/lib/fuse.min.js') .then(() => { console.log('Fuse loaded:', Fuse.version); }) .catch(() => { console.error('Failed to load external library'); }); This function ensures that the library is fully loaded before your plugin logic executes. The file path always follows the plugin path and the location where the library file is stored. Load a library from a CDN Using the same loader function, you can replace the local path with a CDN URL that hosts your library: loadScript('<https://cdn.jsdelivr.net/npm/fuse.js@7.1.0>') .then(() => { console.log('Fuse loaded from CDN'); }); ⚠️ Use CDN loading only if your Sisense environment allows outbound network access and complies with your organization’s security policies. Step 3: Use the library inside the widget or dashboard hooks Once loaded, the library can be used in standard Sisense plugin contexts, such as widgets or dashboard scripts. JavaScript code (widget lodash hook example) widget.on('render', function () { const numbers = [1, 2, 3, 4]; const doubled = _.map(numbers, n => n * 2); console.log(doubled); }); Conclusion:  Using external libraries in Sisense plugins is fully supported when done correctly. Bundling libraries locally is the safest approach, while CDN loading can be used selectively. Always ensure libraries are loaded before execution and test thoroughly across dashboards and widgets. References/Related Content  Loading Amchart5 and Other External Libraries More About The Plugin Manifest How to build Plugins SisenseJS Native Libraries and dependencies 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.

      Micael Santana
      Micael SantanaPosted 6 months ago
      0
               
      • Add-ons & Plug-InsChevronRightIcon

      Custom-style plugin

      Download: Custom Style For more information about CSS, please see Mozilla's developer guide: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference . For more information about Sisense's JavaScript API, please see https://sisense.dev/guides/customJs/jsApiRef/ . What's Included This plugin contains Roboto, a custom font from Google Fonts ( https://fonts.google.com/ ) and the CSS needed to use it in the web application. You can use it as a framework to apply other fonts. It also includes an example of modifying the last build time's font color to green. For information about customizing the indicator, please see https://sisense.dev/guides/customJs/jsApiRef/widgetClass/indicator.html . How To Customize CSS relies on pairs of selectors (web elements to modify) and properties (how to modify the elements). To learn about general selector syntax, see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors . To learn about general available properties, see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Properties_Reference . To see the actual elements Sisense uses in order to use them in your selectors, use your web browser's Developer Tools console to inspect the elements. Advanced Editing There are some restrictions on what elements this plugin (and the Sisense plugin framework) can modify. To make customized modifications to other elements, Sisense's source CSS files are located at C:\Program Files\Sisense\PrismWeb\client\styles. Please note that you make all changes to Sisense source styles at your own risk. In addition, any modifications you make to these files will be overwritten with each upgrade, reinstallation, or repair. Step-by-Step Guide:  Linux: Download the plugin Install the add-on in either one of the following ways: Extract the contents of the .zip folder to the relevant directory below: /opt/sisense/storage/plugins/ OR Select Admin > Server & Hardware > System Management > File Management, and add it to Plugins. NOTE : If File Manager is not enabled, enable it at Admin > App Configuration > Feature Management: https://docs.sisense.com/main/SisenseLinux/feature-management.htm 3. Open "custom.css" file, which is located in /opt/sisense/storage/plugins/CustomStyle 2/styles 4. Edit the CSS file as desired and save the file afterwards. 5. Refresh the dashboard page - it may take a minute for the plugin update to come through. Windows:  Step 1: Download the plugin Download and extract it to the following location: C:\Program Files\Sisense\PrismWeb\plugins. If you are using version 7.2 and higher unzip the contents into your C:\Program Files\Sisense\app\plugins\ folder.  If your version of Sisense is 6.6.1 or lower, restart IIS (run 'iisreset' in the Windows Command Prompt as an administrator) Step 2: Edit the stylesheet Open "custom.css" file which is located in C:\Program Files\Sisense\PrismWeb\plugins\customStyle\styles. Edit the CSS file as desired and save the file afterwards. Refresh the dashboard page - it may take a minute for the plugin update to come through. Disclaimer This plugin has been written and tested in Sisense 7.1. Please note that the elements vary from Sisense version to version. Always ensure you have the license rights to use custom fonts. This plugin is not supported or reviewed with each updated version of Sisense; you use it at your own risk. For troubleshooting questions, please use the comments below or refer to the general web CSS documentation. Update on March 23, 2018 - Updated elements for Sisense 7.x. Added font example and more information about CSS. Last verified on version: L2025.4.0.198

      Anhelina Kravchenko
      Anhelina KravchenkoPosted 8 months ago
      0
               
      • Add-ons & Plug-InsChevronRightIcon

      The Smart Label

                                       

      Download:  Smart Label   Introduction The following article describes steps needed to add to create smart text widget to present ElastiCube fields as text.   Implementaion Steps Extract the folder in the attached zip file into \...\Sisense\PrismWeb\plugins. If you are using version 7.2 and higher unzip the contents into your  C:\Program Files\Sisense\app\plugins\  folder.  Create new widget and chose the smart label icon from the list Choose a field to present, set a design and hit "Apply" [Please note, that in case you have multiple values, it will present the first one. Therefore, it is always better to change the type of the filter to single value only, like in the given example] This plug in works on Version: L8.0.5.156. Only drop the plug in folder contained in the download folder into the plug ins folder in Sisense.  

      intapiuser
      intapiuserPosted 3 years ago • Last reply 8 months ago
      1
               
      • Add-ons & Plug-InsChevronRightIcon

      Bring ElastiCube fields as text on a dashboard with the help of the Smart Label widget [Linux/Windows]

      Implementation steps:  Step 1 - Add the plugin Linux: Download the Smart Label plugin. Install the add-on in either one of the following ways: Extract the contents of the .zip folder to the relevant directory below: /opt/sisense/storage/plugins/ OR Select Admin > Server & Hardware > System Management > File Management, and add it to Plugins. NOTE: If File Manager is not enabled, enable it at Admin > App Configuration > Feature Management: https://docs.sisense.com/main/SisenseLinux/feature-management.htm You may need to wait for up to 5 minutes for the plugin to activate Reload the dashboard for the plugin to appear Windows: Download the Smart Label file and unzip the contents into your C:\Program Files\Sisense\PrismWeb\plugins\ folder. If you are using version 7.2 and higher unzip the contents into your C:\Program Files\Sisense\app\plugins\ folder.  Step 2 - Add the plugin Create a new widget and choose the smart label icon from the list. Choose a field to present, set a design, and hit "Apply." Last verified:  Version L2025.4.0.31 Download link 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.

      Oleksandr H
      Oleksandr HPosted 8 months ago
      0
               
      • Add-ons & Plug-InsChevronRightIcon

      Centering bars and columns when no categories or break by are present in Sisense [linux]

      When a Sisense bar or column chart contains only Values and no items in the Break By or Categories panels, Highcharts — the charting library used by Sisense for rendering visualizations still reserves space for grouped series; this can cause the bars or columns to appear offset from their category labels. By using the beforeviewloaded event, we can modify the Highcharts options for a widget before it is rendered, disabling the grouping property so that the bars or columns are centered with their labels. This improves readability and ensures the chart accurately reflects the data layout. (Applicable to Sisense version 2023.11 and later | Cloud and On-Prem deployments.)             Step-by-Step Guide: Navigate to the dashboard containing the chart you want to adjust. Create a new bar or column chart widget, or select an existing one that meets the conditions (only Values panel populated, no Break By or Categories). Open the Widget Script Editor. Paste the following snippet into the script editor: // Purpose: // Disables Highcharts series grouping for bar/column charts that have no Categories or Break By, // ensuring bars/columns are centered with their labels. function findPanelByTitle(panels, title) { var normalizedTitle = String(title || '').toLowerCase(); return panels.find(function (panel) { return panel && typeof panel.title === 'string' && panel.title.toLowerCase() === normalizedTitle; }); } widget.on('beforeviewloaded', function (widget, env) { var chartOptions = (env && env.options) ? env.options : {}; try { var panels = (widget.metadata && widget.metadata.panels) || []; var isBarOrColumnChart = widget.type === 'chart/bar' || widget.type === 'chart/column'; var breakByPanel = findPanelByTitle(panels, 'break by'); var categoriesPanel = findPanelByTitle(panels, 'categories'); var valuesPanel = findPanelByTitle(panels, 'values'); var breakByIsEmpty = !breakByPanel || !Array.isArray(breakByPanel.items) || breakByPanel.items.length === 0; var categoriesIsEmpty = !categoriesPanel || !Array.isArray(categoriesPanel.items) || categoriesPanel.items.length === 0; var valuesHasItems = !!valuesPanel && Array.isArray(valuesPanel.items) && valuesPanel.items.length > 0; // Apply only if: // - The widget is a bar or column chart // - Break By panel is empty // - Categories panel is empty // - Values panel exists and has items if (isBarOrColumnChart && breakByIsEmpty && categoriesIsEmpty && valuesHasItems) { chartOptions.plotOptions = chartOptions.plotOptions || {}; chartOptions.plotOptions.series = Object.assign({}, chartOptions.plotOptions.series, { grouping: false }); } } catch (_) { // Do nothing if metadata structure is unexpected } }); Save and refresh the widget. If the conditions are met, the bars or columns will now be centered with their labels. Extended Example (Plugin Version): For a solution that automatically applies to all applicable bar and column charts across all dashboards, the following plugin version can be used. This version attaches the same logic globally via the dashboardloaded and widgetinitialized events. /** * Plugin: BarColumnCentering * Purpose: Disables series grouping for bar/column charts with only Values, no Categories or Break By. */ function hasEventHandler(model, eventName, handler) { var eventHandlers = model.$ngscope ? ($$get(prism, '$ngscope.$$listeners.' + eventName) || []) : model.$$eventHandlers(eventName); return eventHandlers.indexOf(handler) >= 0; } function findPanelByTitle(panels, title) { var normalizedTitle = String(title || '').toLowerCase(); return panels.find(function (panel) { return panel && typeof panel.title === 'string' && panel.title.toLowerCase() === normalizedTitle; }); } function onBeforeviewloadedCenter(widget, env) { var chartOptions = (env && env.options) ? env.options : {}; try { var panels = (widget.metadata && widget.metadata.panels) || []; var isBarOrColumnChart = widget.type === 'chart/bar' || widget.type === 'chart/column'; var breakByPanel = findPanelByTitle(panels, 'break by'); var categoriesPanel = findPanelByTitle(panels, 'categories'); var valuesPanel = findPanelByTitle(panels, 'values'); var breakByIsEmpty = !breakByPanel || !Array.isArray(breakByPanel.items) || breakByPanel.items.length === 0; var categoriesIsEmpty = !categoriesPanel || !Array.isArray(categoriesPanel.items) || categoriesPanel.items.length === 0; var valuesHasItems = !!valuesPanel && Array.isArray(valuesPanel.items) && valuesPanel.items.length > 0; if (isBarOrColumnChart && breakByIsEmpty && categoriesIsEmpty && valuesHasItems) { chartOptions.plotOptions = chartOptions.plotOptions || {}; chartOptions.plotOptions.series = Object.assign({}, chartOptions.plotOptions.series, { grouping: false }); } } catch (_) {} } prism.on('dashboardloaded', function (e, args) { args.dashboard.on('widgetinitialized', function (_, initArgs) { var widget = initArgs && initArgs.widget; if (!widget || !widget.on) return; if (!hasEventHandler(widget, 'beforeviewloaded', onBeforeviewloadedCenter)) { widget.on('beforeviewloaded', onBeforeviewloadedCenter); } }); }); The plugin is available for download below and includes a README file with additional details. Conclusion Whether applied to a single widget or deployed globally via a plugin for all widgets, disabling grouping for bar and column charts that have only Values ensures that data is visually aligned with labels. This change reduces confusion and improves chart readability without altering the underlying data. Disclaimer: This post outlines a potential custom workaround for a specific use case or provides instructions regarding a particular 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 10 months ago • Last reply 10 months ago
      1
               
      • Add-ons & Plug-InsChevronRightIcon

      Trend lines (menu based)

      Step-by-Step Guide:  Step 1 - Add the plugin Linux: 1. Download the TrendLine.zip file 2. Install the add-on in either one of the following ways: Extract the contents of the .zip folder to the relevant directory below: /opt/sisense/storage/plugins/ OR Select Admin > Server & Hardware > System Management > File Management, and add it to Plugins. NOTE: If File Manager is not enabled, enable it at Admin > App Configuration > Feature Management: https://docs.sisense.com/main/SisenseLinux/feature-management.htm       3. You may need to wait for up to 5 minutes for the plugin to activate 4. Reload the dashboard for the plugin to appear Windows: Download the TrendLine.zip file and unzip the contents into your C:\Program Files\Sisense\PrismWeb\plugins\ folder. If you are using version 7.2 and higher unzip the contents into your C:\Program Files\Sisense\app\plugins\ folder.  Step 2 - Create or edit your chart Choose Curve Fit >  menu option from the options menu in the chart edit window.  Select the desired regression model. The regression line will be rendered within the chart.      Updated: June 12, 2017 - Power Law Regression Option Added Aug 16, 2018 - LOESS Option Added by Jack Ingoldsby Last verified:  Version L2025.3.0.338 Download link

      Anhelina Kravchenko
      Anhelina KravchenkoPosted 10 months ago
      0
               
    …