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

      Connection Tool - Programmatically Remove Unused Datasource Connections, and List All Connections

                                                                                                                                                                       

      Connection Tool - A Tool to Programmatically Remove Unused Datasource Connections, and List All Connections     Managing connections within your Sisense server can become complex over time, if there are a large number of connections, and connections are often added, and replace earlier datasource connections. In some scenarios unused connections can accumulate, potentially cluttering the Connection Manager UI with no longer relevant connections. Although unused connections typically represent minimal direct security risk, it's considered best practice to maintain a clean, organized list of connections, and in some scenarios it can be desired to remove all unused connections. Sisense prevents the deletion of connections actively used in datasources, safeguarding your dashboards and datasources from disruptions. However, inactive or "orphaned" connections remain after datasources are deleted or a connection is replaced, potentially contributing to unnecessary UI complexity in the connection manager UI. Connections can be of any type Sisense supports, common types include various SQL connections, Excel files, and CSV files, as well as many data providers, such as Big Panda. This tool can also be used to list all connections, with no automatic deletion of unused connections. Introducing the Sisense Connection Prune Tool The Sisense Connection Prune Tool is a Python-based Sisense API based tool designed to programmatically identify and delete unused connections. It generates a CSV report listing all connections and their associated datasources, streamlining your connection management process. If desired, it can automatically remove all unused connections automatically from a Sisense server.   Using the Tool, sourcing the Virtual Environment, generating the Connection CSV   CSV Output of Used Connections and Associated Datasources   CSV opened visually to view as Table, Excel and other programs and text editors can open CSV files   Sisense Connection Prune Tool README Here's the full README included with the tool: # Sisense Connection Prune Tool A command-line tool to list used data connections and prune unused Sisense connections via CSV. It allows you to generate a CSV file of all connections and their dependencies, then delete those connections if needed, after removing the connections to keep from the CSV. ## Features - **Dry Run Mode**: Simulate deletions without making any changes. - **CSV-Based Flow**: Easily inspect and list connections to remove before deletion. - **Logging**: Extensive logs if needed. - **Error Handling**: Clear and descriptive messages for issues encountered during execution. ## Usage 1. **Activate the Virtual Environment** After downloading this project folder, activate the Python virtual environment bundled with it that includes all Python dependencies. - **Windows**: `venv\Scripts\activate` - **macOS/Linux**: `source venv/bin/activate` 2. **Configure the Tool** Open the `config.yaml` file and set your Sisense server URL, bearer token, CSV file path, and log file path. For example: ```yaml server_url: "https://your.sisense.server" bearer_token: "your_bearer_token_here" dry_run: true csv_file_path: "connections.csv" log_file_path: "connection_tool.log" ``` - **server_url**: The URL of your Sisense instance. - **bearer_token**: Your Sisense API token for authentication. - **dry_run**: If set to `true`, deletions will be simulated (no real deletions). - **csv_file_path**: Where the CSV file should be created and read from. - **log_file_path**: Where log file will be stored. 3. **Run the Tool** ```bash python3 ConnectionPruneTool.py ``` You will be prompted to choose an option: 1. **Generate connection CSV** - Fetches all Sisense connections. - Immediately removes (or simulates removing, if `dry_run` is `true`) any connection with no dependencies. - Writes all remaining connections and their dependencies to the CSV file. - **Important**: Inspect the CSV file and remove lines for any connections you want to **keep**. 2. **Delete connections from CSV list** - Reads the CSV file. - Removes or simulates removing each connection still listed. - Provides a summary report of which connections were deleted or bypassed. 4. **Review the Logs** Check the file specified in `log_file_path` for a record of all actions taken or simulated if needed. This is helpful for understanding what happened during each run and diagnosing any issues. ## Example Workflow 1. **Generate CSV** ```bash python3 ConnectionPruneTool.py # Choose option 1 when prompted ``` After generation, open the CSV file and **delete rows** corresponding to any connections you want to **keep**. 2. **Delete Connections** ```bash python3 ConnectionPruneTool.py # Choose option 2 when prompted ``` The tool will read the CSV and delete the remaining listed connections (or simulate deletion, if `dry_run` is enabled). ## Notes - Unused connections are removed automatically in step 1, without a CSV step - To keep a connection, remove its line from the CSV before proceeding with deletion. - If `dry_run` is set to `true`, no actual deletions will occur, only simulated logs and printed messages. - The log file will be cleared at the start of each run, so be sure to review or archive logs (or change log file name in config), if needed. ​ This is a command-line tool to list used data connections and prune unused Sisense connections, in general and via a CSV list. It allows a user with a data admin or higher bearer token to generate a CSV file of all connections and their dependencies, then delete those connections if needed, from the remaining connections in the CSV. Example Output: Deleted unused connection: Old_DB_Connection (ID: 123abc) CSV file generated at connections.csv. It contains 25 row(s) of active connections. Please review and remove lines for connections you want to keep before running deletion step by running tool again. Remaining lines will be deleted in deletion mode. Summary Report: Total lines in CSV (active used connections): 25 Deleted Unused Connections: - Old_DB_Connection No connections were bypassed.   API Endpoints Used Retrieve connections: GET /api/v2/connections Retrieve dependencies: GET /api/v2/connections/{connection_id}/getAllDependencies Delete connection: DELETE /api/v2/connections/{connection_id} Full Code connections.py - Uses Sisense API endpoints to: Fetch all connections (GET /api/v2/connections). Retrieve datasource dependencies for a specific connection (GET /api/v2/connections/{connection_id}/getAllDependencies). Delete a specific connection (DELETE /api/v2/connections/{connection_id}).     from helperFunctions import load_config, api_get, api_delete config = load_config() headers = {"Authorization": f"Bearer {config['bearer_token']}"} base_url = config["server_url"] # Retrieve all connections from Sisense def get_all_connections(): endpoint = f"{base_url}/api/v2/connections" return api_get(endpoint, headers) # Retrieve dependencies of a specific connection def get_connection_dependencies(connection_id): endpoint = f"{base_url}/api/v2/connections/{connection_id}/getAllDependencies" return api_get(endpoint, headers) # Delete a specific connection def delete_connection(connection_id): endpoint = f"{base_url}/api/v2/connections/{connection_id}" return api_delete(endpoint, headers) helperFunctions.py - Uses the Requests library to handle API requests (GET/DELETE). Catches and logs API errors. Uses PyYAML to read the config.yaml file for configuration.     import yaml import requests import logging # Load configuration from YAML file def load_config(): with open("config.yaml", "r") as file: return yaml.safe_load(file) # Configure logging settings def setup_logging(log_file): logging.basicConfig( filename=log_file, level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", ) # Perform a GET request with error handling def api_get(endpoint, headers): try: response = requests.get(endpoint, headers=headers, verify=False) response.raise_for_status() return response.json() except requests.RequestException as e: logging.error(f"GET request failed: {e}") print(f"Error during GET request: {e}") return None # Perform a DELETE request with error handling def api_delete(endpoint, headers): try: response = requests.delete(endpoint, headers=headers, verify=False) response.raise_for_status() return response except requests.RequestException as e: logging.error(f"DELETE request failed: {e}") print(f"Error during DELETE request: {e}") return None ConnectionPruneTool.py - Serves as the entry point for the entire tool. Implements a CLI for user interaction. Invokes connections.py and helperFunctions.py to retrieve, analyze, and optionally delete Sisense connections. Uses Pandas to build a DataFrame of connection details and writes the results to a CSV file. Reads back the CSV file to remove selected connections after manual edits. Manages logs and prints summary reports to the terminal. import pandas as pd import logging import os from connections import ( get_all_connections, get_connection_dependencies, delete_connection, ) from helperFunctions import load_config, setup_logging # Load config yaml config = load_config() setup_logging(config["log_file_path"]) # Clear log file at start open(config["log_file_path"], "w").close() dry_run = config["dry_run"] csv_path = config["csv_file_path"] def generate_connections_csv(): """ Retrieves all connections from Sisense, deletes any that have no dependencies (unless in dry_run mode), and writes the remaining used connections and their dependent data models to a CSV file. """ # Create or clear existing CSV open(csv_path, "w").close() connections = get_all_connections() if connections is None: logging.error("Failed to retrieve connections.") print("Failed to retrieve connections.") return data = [] deleted = [] bypassed = [] # Go through each connection returned from the API for conn in connections: dependencies = get_connection_dependencies(conn["oid"]) if dependencies is None: logging.warning( f"Failed to retrieve dependencies for: {conn['name']} ({conn['oid']})" ) print( f"Failed to retrieve dependencies for: {conn['name']} ({conn['oid']})" ) bypassed.append(conn["name"]) continue # If no dependencies, optionally delete the connection (not in dry-run) if not dependencies: action_msg = "[Dry Run] Would delete" if dry_run else "Deleted" log_str = ( f"{action_msg} unused connection: {conn['name']} (ID: {conn['oid']})" ) print(log_str) logging.info(log_str) if not dry_run: try: response = delete_connection(conn["oid"]) # If response is None, treat it as a failed deletion if response is None: logging.error( f"Failed to delete unused connection: {conn['name']} ({conn['oid']})." ) print( f"Error deleting unused connection: {conn['name']} ({conn['oid']})" ) bypassed.append(conn["name"]) else: deleted.append(conn["name"]) except Exception as e: logging.error( f"Exception while deleting unused connection: {conn['name']} ({conn['oid']}) - {e}" ) print( f"Error deleting unused connection: {conn['name']} ({conn['oid']})" ) bypassed.append(conn["name"]) else: # If the connection is used, add each dependency row to CSV. # Use a fallback value if 'title' or 'oid' is missing. for dep in dependencies: dep_title = dep.get( "title", "NULL TITLE Share Datasource with associated Bearer Token user to include in CSV", ) dep_oid = dep.get("oid", "NULL OID") data.append( { "Connection Name": conn["name"], "Connection ID": conn["oid"], "Elasticube/Data Model Name": dep_title, "Elasticube/Data Model ID": dep_oid, } ) # Create a DataFrame of only the used connections (with datasource dependencies) df = pd.DataFrame(data) df.to_csv(csv_path, index=False) row_count = len(df) logging.info(f"Generated CSV at {csv_path}") print( f"CSV file generated at {csv_path}. " f"It contains {row_count} row(s) of used connections.\n" "Please review and remove lines for connections you want to keep " "before running deletion step by running tool again. Remaining lines will be deleted in deletion mode." ) # Summaries for auto-deleted (unused) connections print("\nSummary Report:") print(f"Total lines in CSV (active used connections): {row_count}") if deleted: print("\nDeleted Unused Connections:") for d in deleted: print(f" - {d}") else: print("\nNo connections were deleted in this step.") if bypassed: print("\nBypassed Connections:") for b in bypassed: print(f" - {b}") else: print("\nNo connections were bypassed in this step.") def delete_connections_from_csv(): """ Reads the CSV (remaining lines after user review, removing lines for connections to keep), then deletes each connection listed. If a delete fails or returns None, the connection is logged and added to 'bypassed'. """ if not os.path.exists(csv_path): print( "CSV file does not exist. Please generate it first or fix the config path." ) return df = pd.read_csv(csv_path) deleted = [] bypassed = [] for _, row in df.iterrows(): conn_id = row["Connection ID"] conn_name = row["Connection Name"] action_msg = "[Dry Run] Would delete" if dry_run else "Deleted" log_str = f"{action_msg} connection: {conn_name} ({conn_id})" print(log_str) logging.info(log_str) if not dry_run: try: response = delete_connection(conn_id) if response is None: logging.error( f"Failed to delete connection: {conn_name} ({conn_id})" ) print(f"Error deleting connection: {conn_name} ({conn_id})") bypassed.append(conn_name) else: deleted.append(conn_name) except Exception as e: logging.error( f"Exception while deleting connection: {conn_name} ({conn_id}) - {e}" ) print(f"Error deleting connection: {conn_name} ({conn_id})") bypassed.append(conn_name) # Print summary report print("\nSummary Report:") if deleted: print("Deleted Connections:") for d in deleted: print(f" - {d}") else: print("No connections were deleted.") if bypassed: print("\nBypassed:") for b in bypassed: print(f" - {b}") else: print("No connections were bypassed.") if __name__ == "__main__": # If there is no CSV file found, select step 1 automatically if not os.path.exists(csv_path): print( "No CSV file found; defaulting to generating connections CSV. " "Correct config if CSV file name has changed." ) generate_connections_csv() else: choice = input( "Choose an option:\n" "1 - Generate connection CSV\n" "2 - Delete connections from CSV list\n" "Enter your choice (1 or 2): " ) if choice == "1": generate_connections_csv() elif choice == "2": delete_connections_from_csv() else: print("Invalid option. Please enter '1' or '2'.") Conclusion By automating the detection of inactive connections and simplifying their removal, the Sisense Connection Prune Tool reduces clutter in the Sisense server Connection Manager UI while minimizing the risk of unintentionally impacting active datasources. Whether you opt for a dry-run mode to review potential deletions in a generated CSV file, or to simply list connections, or proceed with the full removal of unused connections, this tool offers a clear, flexible, and reliable approach to keeping your connections organized. A full copy of the tool, is attached below.        

      Jeremy Friedel
      Jeremy FriedelPosted 1 year ago • Last reply 4 months ago
      4
               
    • Blog banner
      • APIsChevronRightIcon

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

                                                                                                       

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

      Jeremy Friedel
      Jeremy FriedelPosted 1 year ago • Last reply 4 months ago
      3
               
    • Blog banner
      • News & UpdatesChevronRightIcon

      The New Sisense Support Portal is Now Live! 🚀

                       

      🔑 How to Contact Support To ensure your requests are handled as quickly as possible, please use the following channels: AI Chatbot (Preferred) Available directly on the portal, our new smarter AI assistant provides instant answers. If the chatbot cannot solve your issue, it will automatically convert your conversation into a support ticket, ensuring our team has the full context of your request. The Sisense Support Portal Submit and track requests via https://supportportal.sisense.com/ . Why use the portal? Our new smart forms ensure we capture all required technical data immediately, allowing our team to start working on a resolution right away. Support Mailbox You can also report issues by emailing support@sisense.com. Note: While this automatically creates a ticket, our team may follow up with additional questions to gather environment details that are usually captured in the portal forms. Portal Technical Issues? If you experience any difficulties specifically with the portal (login or navigation issues), please notify us at support@sisense.com. 🛠️ Navigating Your New Dashboard Once you log in, you’ll notice several new ways to manage your tickets. Click on "Show Filters" to customize your view: View Types: Switch between Table format (default) or Card view depending on your preference. Ticket Counts: Easily toggle between My Tickets (created by you) and All Tickets (created by your organization, according to the privileges granted for your user before). Customization: You can now Sort, Group, and Filter your tickets by Status, Priority, or Created Date. Quick Tip on Attachments : When creating a new ticket, any files you upload will appear under File Attachments. In existing tickets, your uploaded files will be embedded inline within the ticket comments for better context. Additional emails in CC Additional emails in CC is now live for new and existing tickets. Add email one by one by pressing enter after each. What happens to your old tickets? All open tickets have been successfully migrated. You can find your full ticket history and continue tracking active issues directly within the new portal. Thank you for your patience as we build a better support experience for you!  — The Sisense Support Team

      Amanda Hammar
      Amanda HammarPosted 6 months ago • Last reply 5 months ago
      3
               
      • Sisense AdministrationChevronRightIcon

      GIT Instruction How To

                                               

      GIT Instruction How To In order to migrate Sisense assets between environments, please follow the below steps: Step to add your assets to the GIT repository:   1. Create a project by adding a project name along with a default branch name (such as master, POC, or any meaningful name)   2. Add your GIT repository which could be remote or local.  Below screenshot shows remote repository:      3. When you execute the test to GIT, the system will show connection was successful.       4.  You can either choose the option to create and continue or create and close buttons     5.  You can either share your project with others or continue to the next step:     6. At this point, you have two options to add Assets.  You can add one in below screen and add it to your project:    7.  Or you can save the project close, and add asset by clicking “managing asset”     8. After adding your assets, we should choose the project:   9. Then select all uncommitted changes;  add a comment for your added assets and commit the new changes.     10.  Now you push the assets so the newly added objects will be sent to the repository. 11. The Sisense application will need your GIT application credentials to proceed: Steps to add assets from GIT to another instance: 1. You will need to create a project in the second instance:   Click create project and continue 2. At this step, you can share the new project with other team members if needed: Save and close the project.   3. Then choose pull from your GIT repository and make sure you add the correct branch as seen in below screenshot:   4. The system again will ask for credentials: Where the pull action, will ask for credentials:       5. And it will update the project: 6.  You will then need to pull your added Assets and add your note; and commit afterward: You should then see your assets in the prod instance.  In our ex ample, map elasticube was added. For more information, you can check out the following Articles: https://docs.sisense.com/main/SisenseLinux/introduction-to-sisense-git-integration.htm https://www.sisense.com/platform/git-integration/ https://community.sisense.com/t5/knowledge/elevate-your-data-product-s-quality-with-streamlined-version/ta-p/15131

      Vicki786
      Vicki786Posted 1 year ago • Last reply 1 year ago
      3
               
    • Blog banner
      • Add-ons & Plug-InsChevronRightIcon

      Customizing the Sisense User Interface with Interactive Buttons and Icons

                                                                                                               

      Customizing the Sisense User Interface with Interactive Buttons and Icons Sisense plugins  and scripts enable extensive customization of the Sisense user interface, allowing developers to add interactive elements such as buttons and icons to enhance functionality and user experience. A common use case of plugins involves adding clickable icons or buttons that trigger specific plugin features or open custom UI elements. This article outlines the process for adding these interactive elements using a practical example.   Icon Example Key Steps for Adding Clickable Buttons Follow this general flow to successfully add a custom clickable buttons or icon into the Sisense UI: Choose the UI placement:   Determine the exact area of the Sisense UI where the button or icon will appear. Identify the target parent container:   Find the appropriate parent element in the DOM that will contain the new button. Prevent duplication:   Implement checks to avoid adding duplicate buttons, if Sisense dashboard or prism event used fires more than once. Create the HTML button element:   Construct the button programmatically and apply necessary styling, adding either button text or icon image. Attach Click Listener:   Use JavaScript event listeners to define the button or icon interactive behavior. Practical Example: Adding a Button to the Filter Header Below is a clear and reusable example demonstrating the process of adding a clickable button to the filters header in a Sisense dashboard. This can easily be adapted for different parts of the dashboard or various plugin functionalities.   function addCustomButton() { // Step 1: Locate the UI container (in this example, the header to the right hand filter panel) const filtersContainer = document.querySelector('.filters-headline'); // Step 2: Avoid duplicate button addition if (filtersContainer && !filtersContainer.querySelector('.custom-btn')) { // Identify placement context, in this example next to the spacer element to the right of the filters label const spacerElement = filtersContainer.querySelector('.spacer'); if (spacerElement) { // Step 3: Create the button with appropriate classes const customButton = document.createElement('button'); customButton.classList.add('btn', 'btn--icon', 'btn--dark', 'btn--on-grey', 'custom-btn'); // Insert an SVG icon (example provided) customButton.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0,0,256,256"> <g fill="#5b6372" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"> <g transform="scale(8,8)"> <path d="M9,4c-1.64453,0 -3,1.35547 -3,3v18c0,1.64453 1.35547,3 3,3h17v-24zM9,6h3v11.41406l4,-4l4,4v-11.41406h4v16h-15c-0.35156,0 -0.68359,0.07422 -1,0.1875v-15.1875c0,-0.56641 0.43359,-1 1,-1zM14,6h4v6.58594l-2,-2l-2,2zM9,24h15v2h-15c-0.56641,0 -1,-0.43359 -1,-1c0,-0.56641 0.43359,-1 1,-1z"></path> </g> </g> </svg> `; = ` <!-- Your SVG icon here --> `; // Step 4: Define button functionality customButton.addEventListener('click', function () { // Replace with your plugin's custom action yourPlugin.action(); }); // Insert button into UI spacerElement.insertAdjacentElement('afterend', customButton); } } } // Add button upon dashboard load prism.on("dashboardloaded", function (e, args) { args.dashboard.on("widgetinitialized", addCustomButton); }); By following these principles and adapting the provided example, you can effectively enrich the Sisense interface, tailoring the UI to specific custom workflows and interactions.

      Jeremy Friedel
      Jeremy FriedelPosted 1 year ago
      0
               
      • Sisense AdministrationChevronRightIcon

      Update and add new Highcharts modules for use in Sisense plugins

                                                                                                                       

      Update and add new Highcharts modules for use in Sisense plugins The JavaScript library framework Highcharts is natively included in Sisense and is utilized in many native Sisense widgets as well as in numerous Sisense plugins. Although Sisense typically does not alter the Sisense Highcharts library version with every release, the versions of Highcharts included in Sisense may change when upgrading to a new major version release. Highcharts can load additional chart types and other types of functionality via JS module files that contain code-adding features such as additional chart types, which can be used within plugins along with additional code to create additional widget types. If a plugin utilizes a Highcharts module, you can source the module directly in the "plugin.json" file's source parameter, as shown in this example:   "source": [ "HighchartModule.js", ],   To determine the current Highcharts version being used in your Sisense version, you can use the "Highcharts" command in the web console while viewing any page on your Sisense server. After identifying the current Highcharts version, you can find the corresponding module hosted on a Highcharts code hosting website using the following URL format: https://cdn.jsdelivr.net/npm/highcharts@${Highcharts_Version}/modules/${module_name}.js For example: https://cdn.jsdelivr.net/npm/highcharts@10.3.3/modules/heatmap.js You can save this module and upload it to the plugin folder or replace the older module JS file simply by copying and pasting the code directly. Be sure to update the "plugin.json" file to point to the new module file if the file name has changed or if this is the first time the module is included. Simply sourcing the module file in the "plugin.json" file is sufficient to load the module into Highcharts; no further code is required to load the module.

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

      Loading Amchart5 and Other External Libraries via Script Tags in Plugins

                                                                                                               

      Loading Amchart5 and Other External Libraries via Script Tags in Plugins   This article explains how to load external libraries, such as Amchart5 , into Sisense plugins , such as plugins that create new custom widget visualization types, by dynamically adding script tags to the page header to load the library. This method can avoid potential issues associated with other loading techniques but also offers flexibility such as using an external CDN to reduce plugin size and file count. Previous articles  have discussed how to load external libraries and modules for Sisense plugins via adding the file to the plugin folder, and adding the file to the "source" parameter array in the plugin.json.   What Is a Script Tag?   A   script tag   is an HTML element (<script>) used to embed or reference JavaScript code in an HTML document. When you include a script tag with a   src   attribute, the browser downloads and executes the external JavaScript file.   Why Use Script Tags for Loading External Libraries? For certain JavaScript libraries, especially visualization libraries like Amchart5, loading the library via a script tag can help avoid issues that might arise from bundling the files directly into the plugin. The script tag method provides several benefits: Flexibility:   It allows the option of using an external CDN. This can reduce the size of the plugin package and the number of files you need to manage. Auto-Updating:   When using a CDN, the external library can be updated automatically without modifying the plugin. Self-Hosting Option:   Alternatively, you can set the   src   parameter to a local path of JavaScript files uploaded within the plugin, ensuring that the plugin remains fully self-hosted and independent of any CDN. Loading External Libraries: Self-Hosted or Using a CDN Self-Hosted:   The script’s   src   is set to point to the files within the plugin folder. This follows this very specific format: /plugins/${name_of_plugin_folder}/{Any_subfolders_if_needed}/{full_name_of_the_file} This approach makes the plugin self-contained and avoids external dependencies. The src path must follow this format. CDN-Hosted:   The   src   parameter is set to the URL of the external CDN, such as: https://cdn.amcharts.com/lib/5/xy.js Using a CDN can reduce the plugin’s file size and benefit from auto-updating libraries, though it introduces a dependency on the CDN’s availability. Example Loader Script Below is an example of a loader file (loader.6.js) that dynamically adds script tags to the page header to load Amchart5 and its modules (AM5 is loaded in this example, as well as additional AM5 modules dealing with axises and animation, this is a self-hosted example and does not rely on a CDN). // List of script URLs to add to the page header const scriptUrls = [ "/plugins/am5Example/am5/index.js", "/plugins/am5Example/am5/xy.js", "/plugins/am5Example/am5/themes/Animation.js" ]; // Function to add a script tag to the header function addScriptToHeader(url) { const script = document.createElement("script"); script.src=url; // Optionally set async or defer attributes if needed script.async = false; document.head.appendChild(script); } // Loop through each URL and add it to the header scriptUrls.forEach(url => addScriptToHeader(url));   This can be modified to occur on a specific prism and dashboard event, as opposed to immediately when the plugin loads. In this script: document.createElement("script"):   Creates a new script tag. script.src:   Specifies the source of the JavaScript file. document.head.appendChild(script):   Adds the script tag to the Sisense page header. The   async   attribute is set to   false   to ensure that scripts load in the order they are added. Configuring plugin.json In your plugin’s   plugin.json, reference the loader file and the library file itself. The loader file then adds the necessary external scripts. An example plugin.json configuration is shown below: { "name": "am5Example", "pluginInfraVersion": 2, "isEnabled": true, "source": [ "am5/loader.6.js" ], "folderName": "am5Example", "version": "1.0.0" } This setup makes the external library (Amchart5, in this case) available to your plugin without bundling the entire library directly into the main codebase. Conclusion Using script tags to load external libraries like Amchart5 provides a flexible method for managing dependencies in Sisense plugins. Whether the libraries are self-hosted or rely on an external CDN, this method simplifies the management of external scripts and can lead to more efficient plugin development, and avoid issues with specific libraries such as Amchart5 that work best when loaded as a script element. The example plugin that demonstrates this type of loading is available for download below.    

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

      Limiting Date Filters to Datasource Date Range

                                                                               

      Limiting Date Filters to Datasource Date Range   By default, Sisense allows users to select any date in a dashboard date dimension filter , regardless of whether data exists for that date in the current dashboard datasource. The native Sisense date filter selection UI highlights the earliest and latest dates available via shading and color indications, but if a user selects a date outside this range, it is accepted as the desired filter from the user. While this behavior is generally the preferred behavior, there are cases where a dashboard creator might prefer the date filter to visually and programmatically reflect only the dates for which data exists in the datasource.   Coloring and Shading Indicate Dates Outside Range In scenarios where the exact number of days in the filter range the data being viewed from is vital, such as when analyzing total date data coverage, performing per-day calculations, or simply ensuring that the visual filter UI accurately represents the actual data period being shown, the date filter should ideally be modified to only display date ranges that have corresponding data. This ensures that the filter not only serves as a control mechanism but also as an accurate visual cue of the available data's temporal range. The   dashboard script presented below adds this custom functionality. It works by executing two custom JAQL queries  (More documentation on custom script JAQL queries is available here ) to fetch the sequentially earliest and latest dates from the datasource. These dates are then used as the bounds to restrict the filter. The dimensions used to fetch the date range and the dimension used to find the matching dashboard filter can be separate or identical dimensions, as they are separated in two variables. The script supports both common date filter formats, traditional "from-to" date ranges and filters that use a list of date members. Additionally, the approach can be extended to accommodate other filter types or JavaScript based actions beyond or not including filter modification when a filter's value falls outside the desired range. For more detail and an addition example see the article  Redirect Users to Different Dashboards Based on Dashboard Filters . When a user selects a date that falls outside the available range of the datasource, the code below automatically adjusts the filter. It modifies the selection so that the filter is constrained to the earliest available date if the selected date is too early, or to the latest available date if it exceeds the maximum. If both boundaries are affected, the filter is adjusted on both ends to ensure that only dates with corresponding data are displayed. This solution provides a robust method to visually align your dashboard’s date filter with the actual data available, ensuring that users have a clear and accurate reference of the data period being analyzed. The full code implementing this functionality is provided below. Modifying "from" value to match data date range Same for "To" value Removing date member values that are outside data date range (function () { const enableLogging = true; // Primary date dimension for the filter (change as needed) const dateDim = "[MainTable.Revenue Date (Calendar)]"; // JAQL dimension used to fetch the earliest/latest dates (change as needed) const jaqlDateDim = "[MainTable.Billing Date (Calendar)]"; /** * Simple logging function to enable or disable console logging. */ function log(...msgs) { if (enableLogging) { console.log(...msgs); } } /** * Formats a Date object as "YYYY-MM-DD". */ function formatDate(d) { const yyyy = d.getFullYear(); const mm = String(d.getMonth() + 1).padStart(2, "0"); const dd = String(d.getDate()).padStart(2, "0"); return `${yyyy}-${mm}-${dd}`; } /** * Finds the primary date filter (single-level) based on the defined dateDim. * * Note: * - If run within a dashboard script, the variable "dashboard" is already defined. * - If within a plugin, use prism.activeDashboard. * - If within a widget script, use widget.dashboard. */ function findSingleLevelDateFilter() { if (!dashboard.filters || !dashboard.filters.$$items) return null; return dashboard.filters.$$items.find(filterObj => !filterObj.isCascading && filterObj.jaql && filterObj.jaql.dim === dateDim ); } /** * Constructs a JAQL query to fetch a date from the datasource. * @param {string} direction - "asc" to fetch the earliest date, "desc" to fetch the latest. */ function buildDateQuery(direction) { return { datasource: dashboard.datasource, metadata: [ { jaql: { dim: jaqlDateDim, datatype: "datetime", level: "days", sort: direction } } ], count: 1 }; } /** * Executes an asynchronous HTTP request for the provided JAQL query. */ function runHTTP(jaql) { const $internalHttp = prism.$injector.has("base.factories.internalHttp") ? prism.$injector.get("base.factories.internalHttp") : null; const ajaxConfig = { url: `/api/datasources/${encodeURIComponent(jaql.datasource.title)}/jaql`, method: "POST", data: JSON.stringify(jaql), contentType: "application/json", dataType: "json", async: true, xhrFields: { withCredentials: true } }; return $internalHttp ? $internalHttp(ajaxConfig, false) : $.ajax(ajaxConfig); } /** * Adjusts the date filter so that its values fall within the datasource range. * For multi-valued filters (using a "members" array), out-of-range dates are removed. * For single-valued filters with "from" and "to" fields, each is updated if outside the available range. * * @param {Object} filterObj - The primary date filter object. * @param {Date} earliestDate - The earliest available date. * @param {Date} latestDate - The latest available date. */ function adjustDateFilterIfOutOfRange(filterObj, earliestDate, latestDate) { if (!filterObj || !filterObj.jaql || !filterObj.jaql.filter) return; const jaqlFilter = filterObj.jaql.filter; let adjustmentMade = false; // Adjust multi-valued filter (members). if (Array.isArray(jaqlFilter.members) && jaqlFilter.members.length > 0) { const originalCount = jaqlFilter.members.length; const validDates = jaqlFilter.members.filter(dateStr => { const d = new Date(dateStr); return !isNaN(d.valueOf()) && (!earliestDate || d >= earliestDate) && (!latestDate || d <= latestDate); }); if (validDates.length < originalCount) { jaqlFilter.members = validDates; adjustmentMade = true; log("Adjusted members filter to valid dates:", validDates); } } // Adjust "from" date if necessary. if (typeof jaqlFilter.from === "string") { const fromDate = new Date(jaqlFilter.from); if (earliestDate && fromDate < earliestDate) { jaqlFilter.from = formatDate(earliestDate); adjustmentMade = true; log("Adjusted 'from' date to:", jaqlFilter.from); } } // Adjust "to" date if necessary. if (typeof jaqlFilter.to === "string") { const toDate = new Date(jaqlFilter.to); if (latestDate && toDate > latestDate) { jaqlFilter.to = formatDate(latestDate); adjustmentMade = true; log("Adjusted 'to' date to:", jaqlFilter.to); } } if (adjustmentMade) { log("Date filter adjusted for dimension:", dateDim); } } /** * Retrieves the earliest and latest dates from the datasource, * then adjusts the primary date filter so that its values fall within that range. */ function updateDateFilter() { const queryEarliest = buildDateQuery("asc"); const queryLatest = buildDateQuery("desc"); Promise.all([runHTTP(queryEarliest), runHTTP(queryLatest)]) .then(([responseEarliest, responseLatest]) => { let earliestDate = null; let latestDate = null; if (responseEarliest && responseEarliest.data && responseEarliest.data.values?.length) { const eStr = responseEarliest.data.values[0][0].data; const dt = new Date(eStr); if (!isNaN(dt.valueOf())) { earliestDate = dt; log("Earliest date from datasource:", formatDate(dt)); } } if (responseLatest && responseLatest.data && responseLatest.data.values?.length) { const lStr = responseLatest.data.values[0][0].data; const dt = new Date(lStr); if (!isNaN(dt.valueOf())) { latestDate = dt; log("Latest date from datasource:", formatDate(dt)); } } const filterObj = findSingleLevelDateFilter(); if (!filterObj) { log("No primary date filter found; cannot adjust date filter."); return; } adjustDateFilterIfOutOfRange(filterObj, earliestDate, latestDate); }) .catch(err => { log("Error fetching datasource date range:", err); }); } // Call updateDateFilter() when filters change. dashboard.on('filterschanged', function () { updateDateFilter(); }); // Call updateDateFilter() on dashboard load dashboard.on('initialized', function () { updateDateFilter(); }); })();   Console Output of Script modifying filter to match data date range

      Jeremy Friedel
      Jeremy FriedelPosted 1 year ago
      0
               
      • Add-ons & Plug-InsChevronRightIcon

      Exploring the Potential of Sisense Jump to Dashboard Filter Configurations

                                                                       

      Exploring the Potential of Sisense Jump to Dashboard Filter Configurations Introduction: Sisense Jump to Dashboard offers a powerful way to enhance the user experience and streamline data exploration with the help of different filter configurations. By default, all the filters from the parent dashboard, measured values, and widget filters are passed and replaced in the drill dashboard. This guide explains and provides examples of how you can customize the way filters impact the drill dashboard. We'll delve into multiple filter configuration options and provide a step-by-step guide on how to implement them effectively. The Default Behaviour & Description: The default settings of the Jump to Dashboard add-on do not require any JavaScript configuration. You can use the plugin with the default configuration right after it’s enabled. The default filter behavior configuration is the following: displayFilterPane : true This parameter determines if to display a filter pane in the target dashboard window. The default value is true which means that the filters pane shall be displayed. excludeFilterDims : [] Dimensions to exclude from the drilled dashboard filter. An empty array means that no dimensions are excluded. includeFilterDims : [] Dimensions to include in the drilled dashboard filter. An empty array means that all dimensions are included. resetDashFilterAfterJTD : false Resets the filters of a target dashboard. Combine this with mergeTargetDashboardFilters if you want your dashboard filters to be displayed in the target dashboard. mergeTargetDashboardFilters : false Determines if you want your dashboard filters to be displayed in the target dashboard (usually combined with resetDashFilterAfterJTD) Configuration Methods: A detailed explanation can be found here: https://community.sisense.com/t5/knowledge/jumptodashboard-plugin-how-to-use-and-customize/ta-p/17375 config.js method: it’s a system-wide configuration and takes effect for the entire Sisense installation. To edit the default system-wide JTD filters configuration set in the config.js navigate with the help of File Manager to plugins -> jumpToDashboard -> js -> config.js Adjust the configuration required by modifying the file and saving the changes - the changes will take effect after the plugin rebuild process which is initiated right after the config changes are saved. Once the user refreshes the dashboard and obtains the last build of the plugins, the new configuration will be in place across all the dashboards. Widget script method: It’s used to override the default system-wide config and can change the behavior of a particular widget Not all configuration parameters are supported for this method - check the “Configured In” field of the specification table located on the Technical Details tab of the JTD Marketplace Page: Jump to Dashboard - Sisense Configuration can be applied by adding the Widget JavaScript Extension to the particular widget The changes take effect right after the script is saved and the dashboard containing the widget is reloaded. Configurable Filters Behavior + Script Examples: displayFilterPane The filter panel is visible by default: If you want to hide it in the drill dashboard, just change row 4 in config.js like:   displayFilterPane : false   OR add the following script to configure it for the particular JTD widget   prism.jumpToDashboard(widget, { displayFilterPane: false});​   The result will look like this: excludeFilterDims By default, all the filters from the parent dashboard, measured values, and widget filters are passed to the drill dashboard, but you can exclude particular dimensions from being passed. In Sisense, a "dimension" is a qualitative attribute used to categorize and filter quantitative data (measures). In most cases, it will be represented as the table_name.column_name identifying the data location in the elasticube or live model. A few things to note: excludeFilterDims is an array so even when the single filter dimension is used, it should be enclosed in square brackets []. This configuration can be modified both on a system-wide level via config.js, or via the widget script When excluding the date dimension using the parameter excludeFilterDims, (Calendar) must be used or the exclusion will not work.  Example: [Table.Dimension(Calendar)] An example if you use excludeFilterDims with Dimension B: Parent Dash FIlters Drill Dash Filters Resulting Filters Dimension A - Value A1 Dimension A - Value A2 Dimension A - Value A1 Dimension B - Value B1 Dimension B - Value B2 No Dimension C - Value C1 Dimension C - Value C2 Dimension C - Value C1 Widget script Example:   prism.jumpToDashboard(widget, { excludeFilterDims: ["[divisions.Divison_name]", "[Admissions.Admission_Time (Calendar)]", "[doctors.Specialty]" ] });   includeFilterDims It’s the opposite of the excludeFilterDims described above. includeFilterDims is intended to explicitly set the filter's dimensions you want to pass to the drill dashboard. All other filter dims will be ignored . The same usage notes apply here as for the excludeFilterDims. Example if you use includeFilterDims with Dimension B: Parent Dash FIlters Drill Dash Filters Resulting Filters Dimension A - Value A1 Dimension A - Value A2 No Dimension B - Value B1 Dimension B - Value B2 Dimension B - Value B1 Dimension C - Value C1 Dimension C - Value C2 No Widget script Example:   prism.jumpToDashboard(widget, { includeFilterDims: ["[country.Country]", "[brand.Brand]" ] });   resetDashFilterAfterJTD Note: Configurable in config,js file only By default, after we open the dashboard with the help of JTD, the filters passed to this drill dashboard by JTD are saved for the user. This behavior can be changed with the help of resetDashFilterAfterJTD config. Once set to true, the filters of the drill dashboard will be preserved (in the temporary storage inside the dashboard object ​​prism.activeDashboard.filtersToRestore) and restored during the next dashboard opening. config.js example:   resetDashFilterAfterJTD: true   mergeTargetDashboardFilters By default, when the drill dashboard is opened with the help of JTD, the filters of the drill dashboard are replaced with the filters from the parent dashboard. If you’d like to compliment the dashboard filters with the original ones from the drill dashboard, you can enable this parameter. Usage Example: Parent Dash FIlters Drill Dash Filters mergeTargetDashboardFilters: false mergeTargetDashboardFilters: true No Dimension A - Value A2 No Dimension A - Value A2 Dimension B - Value B1 Dimension B - Value B2 Dimension B - Value B1 Dimension B - Value B1 No Dimension C - Value C2 No Dimension C - Value C2 Widget Script Example:   prism.jumpToDashboard(widget, { mergeTargetDashboardFilters: true });   Locating the correct dimension for the config: There is a simple way of finding out the correct filter dims for the configuration scripts. When the source dashboard is open, open the Browser Development Console. Here are the common shortcuts to open the browser developer console: Chrome: Ctrl + Shift + J (Windows/Linux) or Cmd + Option + J (Mac) Firefox: Ctrl + Shift + K (Windows/Linux) or Cmd + Option + K (Mac) Edge: Ctrl + Shift + I (Windows/Linux) or Cmd + Option + I (Mac) Safari: Cmd + Option + C (Mac) (Enable "Show Develop menu in menu bar" in Preferences first) Opera: Ctrl + Shift + I (Windows/Linux) or Cmd + Option + I (Mac) In the console type in the following to list the active dashboard filters’ dimensions:   prism.activeDashboard.filters.$$items.forEach((item)=>console.log(JSON.stringify(item.jaql.dim)));   The list of active dashboard filter dimensions should be returned like below, so you can use them in your configurations: Hope the above helps you understand the Jump to Dashboard filter settings better and I wish you good luck with setting up your JTD customizations!

      Taras Skvarko
      Taras SkvarkoPosted 2 years ago • Last reply 1 year ago
      2
               
    • Blog banner
      • Add-ons & Plug-InsChevronRightIcon

      Plugin - RemoveImageDownload - Removing Items From Sisense Menus

                                                                                                               

      Plugin – RemoveImageDownload – Removing Items From Sisense Menus This article discusses a  plugin (and an equivalent dashboard script ) that removes the “Download as Image” option from Sisense menus. This same approach can be applied to remove any other menu option in Sisense by adjusting the relevant code. Organizations may want to hide or remove specific menu items for several reasons: Security : Prevent certain menu options from being used. Enforcing Best Practices:  Remove menu items not used in the standard recommended workflow Streamlined UI : Hide unused menu items to simplify the user experience. Plugin Overview RemoveImageDownload  plugin removes the “Download Image” option from all standard Sisense menus which include the: Dashboard Toolbar Menu Widget Context Menu (in a dashboard) Direct Download Menu in the Widget Editor and Viewer   This is accomplished using the Sisense beforemenu event . That event runs before any standard Sisense menu is rendered, giving scripts and plugins an opportunity to modify or remove items. The primary JavaScript file of the plugin (main.6.js) can also be used as a standalone dashboard script, simply copy and paste the code as a dashboard script. The code works by listening to the beforemenu event, which Sisense triggers before rendering any of its menus. Within that event, the script functions filters the args.settings.items array, each “item” in this array may itself contain nested sub-items (like the “Download” submenu). The script locates the specific item or items to remove by checking parameters such as item.caption or item.command.title, and then filters the menu items out accordingly. This code design is flexible, by adjusting which string is matched in the filter logic this code can be used for any standard Sisense menu item.   Below is the complete code, with explanatory comments:             // // Remove the image option from the download menu in the widget context (dashboard view) // prism.on("beforemenu", (ev, args) => { if ( !args.settings?.name || !args.settings?.scope?.widget || !args.settings.name.includes("widget") || !args.settings.items ) { return; } const downloadMenu = args.settings.items.find(item => item.caption === "Download"); if (!downloadMenu?.items) { return; } const downloadWithoutImage = downloadMenu.items.filter(item => { return !(item.command && item.command.title === "dashboard.widget.commands.image.title"); }); if (downloadWithoutImage !== undefined) { downloadMenu.items = downloadWithoutImage; } }); // // Remove the "Download Image" option from the main dashboard Download menu // prism.on("beforemenu", (ev, args) => { if ( !args.settings?.name || !args.settings.name.includes("dashboard") || args.settings.name.includes("widget") || !args.settings.items ) { return; } const downloadMenu = args.settings.items.find(item => item.caption === "Download"); if (!downloadMenu?.items) { return; } const downloadWithoutImage = downloadMenu.items.filter(item => { return !(item.command && item.command.title === "Download Image"); }); if (downloadWithoutImage !== undefined) { downloadMenu.items = downloadWithoutImage; } }); // // Remove the image download option from the Widget Editor UI download dropdown // prism.on("beforemenu", (ev, args) => { if (!args.settings?.items) { return; } args.settings.items = args.settings.items.filter(item => { return !(item.command && item.command.title === "dashboard.widget.commands.image.title"); }); });               Plugin Readme Below is the README for the RemoveImageDownload plugin.               # Remove Image Download from Menu ## Description This plugin removes the option to download images from the widget and dashboard menu UI. It does not modify Sisense API endpoints, including the image API endpoint. ## Installation 1. **Download the plugin:** - Extract the compressed archive to /opt/sisense/storage/plugins/ - Or, in Admin > System Management > File Management, upload the extracted folder to the plugins directory. 2. **Wait for the plugin to load.** 3. **Refresh the page.**             Screenshots Below are before-and-after screenshots of the relevant Sisense menus when the plugin is enabled. With Plugin   Without Plugin   With Plugin Without Plugin   With Plugin Without Plugin Further Customization and Other Use Cases To use this code to hide additional items or different item adjust the strings checked in item.command.title or item.caption to hide or rename other menu items. Console logging the args.settings.items array temporarily can be used to find the appropriate title and caption strings.The conditionals can be modified as needed to only apply the function to a particular menu. The same beforemenu event can be used to modify Sisense menus as needed in Sisense scripts and plugins.  The plugin is downloadable below.   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 1 year ago
      0