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
    FAQ: Plugins
    • Blog banner
      • PySisenseChevronRightIcon

      Managing Sisense Plugin States with PySisense

                                                                                                       

      Managing Sisense Plugin States with PySisense Overview Enabling and disabling Sisense plugins through the admin interface becomes time consuming when maintaining consistent plugin configurations across environments, testing against a clean instance, or iterating on plugin development. This article describes plugin_manager_pysisense.py , a Python command-line tool that manages Sisense plugin states using the PySisense library and a configuration file. The script is also a working example of how PySisense can be used for Sisense automation. This tool covers the same functionality as plugin_manager.py , documented in Managing Sisense Plugin States via the REST API . The difference is that this version delegates all API request code to PySisense rather than using the requests library directly. The two scripts share the same config.yaml format and produce identical output. The script enables and disables plugins, isolates a single plugin for faster development builds, and saves and restores named snapshots of plugin state. It does not install, uninstall, or update plugins, and does not roll back plugin versions. About PySisense PySisense is introduced in PySisense: Programmable Sisense Environment Management on the Sisense Community as a Python SDK for the Sisense REST API. It provides wrappers around common Sisense operations, including plugin management, dashboard access, and user administration. It is installed via pip and does not need to be placed alongside the script. This script provides a practical example of PySisense applied to a real automation task. Using PySisense rather than writing directly against the REST API reduces the amount of code required and removes several categories of boilerplate. Authentication headers, HTTP session management, SSL configuration, pagination across large result sets, and request logging are all handled by the library. The consistent SisenseClient and module pattern also makes it straightforward to extend a script to cover other Sisense operations, such as users, dashboards, or data models, without reworking connection and authentication code. Scripts built on PySisense are shorter and easier to read than equivalent scripts built on requests directly, and the library is maintained and tested independently of the scripts that use it. Note: The current release of PySisense on PyPI does not yet include the Plugins module used in this script. This functionality is planned for the next PySisense release. The development branch of the PySisense repository already contains it and can be installed directly from there. This script uses the Plugins class from PySisense: get_all_plugins() returns the full plugin list as a list of dicts, handling the pagination in the underlying API automatically. enable_plugins(names, bulk=True) enables one or more plugins by name. When bulk=True , all changes are sent in a single PATCH request. disable_plugins(names, bulk=True) disables one or more plugins by name, with the same bulk option. A SisenseClient instance is constructed from the config values and passed to the Plugins class. All HTTP calls are then handled by PySisense. from pysisense import Plugins, SisenseClient client = SisenseClient(domain="https://your-sisense-instance.com", token="YOUR_TOKEN") plugins = Plugins(api_client=client) # List all installed plugins all_plugins = plugins.get_all_plugins() # Enable plugins by short name, full name, or folderName result = plugins.enable_plugins(["SwitchDimension", "plugin-CustomPlugin"], bulk=True) # Disable plugins result = plugins.disable_plugins(["UnneededPlugin"], bulk=True) Both enable_plugins and disable_plugins return a summary dict with changed , already_enabled or already_disabled , not_found , and errors keys. Features that require knowing the current plugin state before making changes, such as strict mode, enable-one, and dry-run previews, call get_all_plugins() once at the start of the command and compute the set of changes at the script level. Plugin name matching Plugin names in the configuration file and on the command line accept either the short form ( SwitchDimension ) or the full form ( plugin-SwitchDimension ). Matching is case-insensitive. PySisense normalizes names internally, so both forms work with enable_plugins() and disable_plugins() as well. Prerequisites Python 3.10 or later Network access to the Sisense instance (the tool runs remotely and does not need to be installed on the server) A Sisense API token: Admin > REST API > v1.0 > GET /authentication/tokens/api Setup Install the required Python packages: pip install pysisense 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 . 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_pysisense.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_pysisense.py --restore-snapshot To preview what would change without making any API calls: python plugin_manager_pysisense.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_pysisense.py --strict Testing on a stock Sisense instance Plugin interference is a common source of issues that are difficult 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_pysisense.py --strict # restore when done python plugin_manager_pysisense.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_pysisense.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_pysisense.py --save-snapshot before-upgrade # list all saved snapshots python plugin_manager_pysisense.py --list-snapshots # restore a specific snapshot python plugin_manager_pysisense.py --restore-snapshot before-upgrade Including --log-version when saving a snapshot records the version of each plugin at that point in time. Snapshots saved by plugin_manager_pysisense.py and plugin_manager.py are compatible. Both scripts read from the same snapshots/ directory, and each handles the other's snapshot format. Full option reference usage: plugin_manager_pysisense.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 PySisense. 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 --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. --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 PySisense 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 Demonstrates how PySisense can be used to build automation scripts for Sisense instances 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. The script also serves as a concrete example of how PySisense can be used in automation scripts: constructing a SisenseClient from config values, passing it to a feature class, and using the returned result dicts to drive output and error handling. The code is not compressed or obfuscated and can be modified as needed or used as a reference for similar tools. j""" plugin_manager_pysisense.py — Manage Sisense plugin states via pySisense. Same feature set as plugin_manager.py but delegates all API calls to the pySisense library instead of calling the Sisense REST API directly. Configuration is read from config.yaml. This script is also a working example of how pySisense can be used for Sisense automation. Install pySisense with: pip install pysisense Overview Manages Sisense plugin states via pySisense. 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 Clear enable_plugins in config.yaml, run --strict to disable everything, test, then restore: python plugin_manager_pysisense.py --strict python plugin_manager_pysisense.py --restore-snapshot Develop and iterate on a single plugin quickly python plugin_manager_pysisense.py --enable-one MyPlugin Roll back any change instantly python plugin_manager_pysisense.py --list-snapshots python plugin_manager_pysisense.py --restore-snapshot before-upgrade Usage # Enable listed plugins, disable listed disable_plugins, leave rest unchanged python plugin_manager_pysisense.py # Strict: also disables any enabled plugin not in enable_plugins python plugin_manager_pysisense.py --strict # Preview what would change (no API calls, no auto snapshot) python plugin_manager_pysisense.py --dry-run python plugin_manager_pysisense.py --strict --dry-run # Run only one half of the sync python plugin_manager_pysisense.py --enable-only # enable listed; skip all disables python plugin_manager_pysisense.py --disable-only # disable listed; skip all enables # Enable exactly one plugin and disable every other currently enabled plugin python plugin_manager_pysisense.py --enable-one SwitchDimension # Snapshots python plugin_manager_pysisense.py --save-snapshot [NAME] python plugin_manager_pysisense.py --save-snapshot [NAME] --log-version python plugin_manager_pysisense.py --list-snapshots python plugin_manager_pysisense.py --restore-snapshot [NAME] # omit NAME for 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 pysisense pyyaml # Python 3.10+ 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 import yaml from pysisense import Plugins, SisenseClient SCRIPT_DIR = Path(__file__).parent DEFAULT_CONFIG = SCRIPT_DIR / "config.yaml" SNAPSHOTS_DIR = SCRIPT_DIR / "snapshots" # Config def load_config(path: Path) -> dict: if not path.exists(): print(f"Config file not found: {path}") print("Copy config.yaml.example to config.yaml and fill in your instance URL and API token.") sys.exit(1) with path.open() as f: return yaml.safe_load(f) def build_client(sisense_cfg: dict) -> SisenseClient: url = sisense_cfg.get("url", "") token = sisense_cfg.get("token", "") if not url or not token: print("config.yaml must include sisense.url and sisense.token.") sys.exit(1) is_ssl = not url.startswith("http://") return SisenseClient(domain=url, token=token, is_ssl=is_ssl) # Name matching — mirrors pySisense internals, used for dry-run previews and # strict-mode computation (pySisense handles matching internally for real calls) def _normalize(name: str) -> str: lower = name.lower() return lower[len("plugin-"):] if lower.startswith("plugin-") else lower def _build_lookup(names: list) -> set: lookup: set = set() for n in names: norm = _normalize(n) lookup.add(norm) lookup.add("plugin-" + norm) return lookup def _matches_lookup(plugin: dict, lookup: set) -> bool: return bool({plugin["folderName"].lower(), plugin["name"].lower()} & lookup) # Snapshot file I/O def write_snapshot_file(name: str, snapshot: dict) -> Path: SNAPSHOTS_DIR.mkdir(exist_ok=True) path = SNAPSHOTS_DIR / f"{name}.yaml" with path.open("w") as f: yaml.dump(snapshot, f, sort_keys=False, default_flow_style=False, allow_unicode=True) return path def read_snapshot_file(name: str) -> dict: path = SNAPSHOTS_DIR / f"{name}.yaml" if not path.exists(): print(f"Snapshot not found: {path}") sys.exit(1) with path.open() as f: data = yaml.safe_load(f) # Translate 'enable_plugins' key (plugin_manager.py snapshot format) to 'plugins' if "enable_plugins" in data and "plugins" not in data: data["plugins"] = data["enable_plugins"] return data def list_snapshot_files() -> list: if not SNAPSHOTS_DIR.exists(): return [] return sorted(SNAPSHOTS_DIR.glob("*.yaml")) def latest_snapshot_name() -> str: files = list_snapshot_files() if not files: print("No snapshots found.") sys.exit(1) def created_at(path: Path) -> str: with path.open() as f: return yaml.safe_load(f).get("created", "") return max(files, key=created_at).stem def _auto_save_snapshot(all_plugins: list, include_version: bool) -> None: name = datetime.now().strftime("auto_%Y%m%d_%H%M%S") print(f"\nSaving snapshot '{name}' before applying changes...") try: enabled = sorted(p["folderName"] for p in all_plugins if p.get("isEnabled")) disabled = sorted(p["folderName"] for p in all_plugins if not p.get("isEnabled")) snapshot: dict = { "plugins": enabled, "created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), } if include_version: versions: dict = {} for p in all_plugins: v = p.get("version") or p.get("pluginVersion") if v: versions[p["folderName"]] = str(v) if versions: snapshot["plugin_versions"] = versions path = write_snapshot_file(name, snapshot) print(f" Saved to {path} ({len(enabled)} enabled / {len(disabled)} disabled)") except Exception as exc: print(f" Warning: auto snapshot failed: {exc}") # Sync computation — determines which plugins need their state changed. # Used for dry-run previews, strict mode, and enable-one. def _compute_updates( all_plugins: list, enable_names: list, disable_names: list, *, strict: bool, enable_only: bool, disable_only: bool, ) -> tuple: enable_lookup = _build_lookup(enable_names) disable_lookup = _build_lookup(disable_names) matched_enable = [p for p in all_plugins if _matches_lookup(p, enable_lookup)] enable_folder_set = {p["folderName"] for p in matched_enable} to_enable: list = [] if disable_only else sorted( p["folderName"] for p in matched_enable if not p.get("isEnabled") ) if enable_only: to_disable: list = [] elif strict: to_disable = sorted( p["folderName"] for p in all_plugins if p.get("isEnabled") and p["folderName"] not in enable_folder_set ) else: to_disable = sorted( p["folderName"] for p in all_plugins if p.get("isEnabled") and _matches_lookup(p, disable_lookup) ) matched_norms = ( {_normalize(p["folderName"]) for p in matched_enable} | {_normalize(p["name"]) for p in matched_enable} ) unmatched = sorted(n for n in enable_names if _normalize(n) not in matched_norms) return to_enable, to_disable, unmatched # Output helpers def _print_update_plan( to_enable: list, to_disable: list, already_correct: int, untouched: int = 0, ) -> None: print(f" To enable: {len(to_enable)}") print(f" To disable: {len(to_disable)}") if already_correct > 0: print(f" Already set: {already_correct}") if untouched > 0: print(f" Untouched: {untouched} (not listed in config; left as-is)") print() def _print_dry_run_preview(to_enable: list, to_disable: list) -> None: if not to_enable and not to_disable: print("Nothing to change.") if to_enable: print("Would enable:") for f in to_enable: print(f" + {f}") if to_disable: print("Would disable:") for f in to_disable: print(f" - {f}") print("\n[DRY RUN] No changes made.") def _report_unmatched(unmatched: list) -> None: if unmatched: print(f"\nNot found in instance ({len(unmatched)}):") for n in unmatched: print(f" - {n}") def _apply_enable(plugins_obj: Plugins, to_enable: list, bulk: bool) -> int: if not to_enable: return 0 result = plugins_obj.enable_plugins(to_enable, bulk=bulk) if "error" in result: print(f"Failed to enable plugins: {result['error']}") sys.exit(1) for f in result["changed"]: print(f" [ENABLED] {f}") return len(result.get("errors", [])) def _apply_disable(plugins_obj: Plugins, to_disable: list, bulk: bool) -> int: if not to_disable: return 0 result = plugins_obj.disable_plugins(to_disable, bulk=bulk) if "error" in result: print(f"Failed to disable plugins: {result['error']}") sys.exit(1) for f in result["changed"]: print(f" [DISABLED] {f}") return len(result.get("errors", [])) # Commands def cmd_sync_to_config( plugins_obj: Plugins, enable_names: list, disable_names: list, *, dry_run: bool, bulk: bool, strict: bool, enable_only: bool, disable_only: bool, skip_snapshot: bool, include_version: bool, ) -> None: print("Fetching plugin list...") all_plugins = plugins_obj.get_all_plugins() if all_plugins and "error" in all_plugins[0]: print(f"Failed to fetch plugins: {all_plugins[0]['error']}") sys.exit(1) print(f" {len(all_plugins)} plugins total\n") to_enable, to_disable, unmatched = _compute_updates( all_plugins, enable_names, disable_names, strict=strict, enable_only=enable_only, disable_only=disable_only, ) enable_lookup = _build_lookup(enable_names) disable_lookup = _build_lookup(disable_names) matched = [p for p in all_plugins if _matches_lookup(p, enable_lookup)] already_correct = sum(1 for p in matched if p.get("isEnabled")) untouched = 0 if strict or enable_only else sum( 1 for p in all_plugins if not _matches_lookup(p, enable_lookup) and not _matches_lookup(p, disable_lookup) ) if matched: print(f"Matched {len(matched)} plugin(s) from enable_plugins:\n") print(f" {'Folder name':<45} {'API name':<35} {'Enabled'}") print(f" {'-'*45} {'-'*35} {'-'*7}") for p in sorted(matched, key=lambda x: x["folderName"]): print(f" {p['folderName']:<45} {p['name']:<35} {p['isEnabled']}") print() _print_update_plan(to_enable, to_disable, already_correct, untouched) if dry_run: _print_dry_run_preview(to_enable, to_disable) _report_unmatched(unmatched) return if not skip_snapshot and (to_enable or to_disable): _auto_save_snapshot(all_plugins, include_version) errors = _apply_disable(plugins_obj, to_disable, bulk) + _apply_enable(plugins_obj, to_enable, bulk) if not to_enable and not to_disable: print("Nothing to change.") else: print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {errors}") _report_unmatched(unmatched) def cmd_enable_one( plugins_obj: Plugins, plugin_name: str, *, dry_run: bool, bulk: bool, skip_snapshot: bool, include_version: bool, ) -> None: target_lookup = _build_lookup([plugin_name]) print("Fetching plugin list...") all_plugins = plugins_obj.get_all_plugins() if all_plugins and "error" in all_plugins[0]: print(f"Failed to fetch plugins: {all_plugins[0]['error']}") sys.exit(1) print(f" {len(all_plugins)} plugins total\n") target = next((p for p in all_plugins if _matches_lookup(p, target_lookup)), None) if target 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["folderName"] target_on = target.get("isEnabled", False) to_enable = [] if target_on else [target_folder] to_disable = sorted( p["folderName"] for p in all_plugins if p.get("isEnabled") and p["folderName"] != target_folder ) print(f"Target plugin: {target_folder}") print(f" Currently: {'enabled' if target_on else 'disabled'}\n") _print_update_plan(to_enable, to_disable, already_correct=1 if target_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) errors = _apply_disable(plugins_obj, to_disable, bulk) + _apply_enable(plugins_obj, to_enable, bulk) if not to_enable and not to_disable: print("Nothing to change.") else: print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {errors}") def cmd_save_named_snapshot(plugins_obj: Plugins, name: str, include_version: bool) -> None: print("Fetching plugin list...") all_plugins = plugins_obj.get_all_plugins() if all_plugins and "error" in all_plugins[0]: print(f"Failed to fetch plugins: {all_plugins[0]['error']}") sys.exit(1) print(f" {len(all_plugins)} plugins total\n") enabled = sorted(p["folderName"] for p in all_plugins if p.get("isEnabled")) disabled = sorted(p["folderName"] for p in all_plugins if not p.get("isEnabled")) snapshot: dict = { "plugins": enabled, "created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), } if include_version: versions: dict = {} for p in all_plugins: v = p.get("version") or p.get("pluginVersion") if v: versions[p["folderName"]] = str(v) if versions: snapshot["plugin_versions"] = versions path = write_snapshot_file(name, snapshot) print(f"Snapshot '{name}' saved to {path}") print(f" {len(enabled)} enabled / {len(disabled)} disabled plugin(s) captured.") if include_version and snapshot.get("plugin_versions"): print(f" Versions recorded for {len(snapshot['plugin_versions'])} plugin(s).") def cmd_list_snapshots() -> None: files = list_snapshot_files() if not files: print("No snapshots found.") return print(f"Snapshots ({len(files)}):\n") print(f" {'Name':<32} {'Created':<22} {'Enabled':<9} {'Versions'}") print(f" {'-'*32} {'-'*22} {'-'*9} {'-'*8}") for path in files: with path.open() as f: data = yaml.safe_load(f) enabled_count = len(data.get("plugins") or data.get("enable_plugins", [])) versions_logged = "yes" if data.get("plugin_versions") else "no" print( f" {path.stem:<32} " f"{data.get('created', 'unknown'):<22} " f"{enabled_count:<9} " f"{versions_logged}" ) def cmd_restore_snapshot( plugins_obj: Plugins, name: str, *, dry_run: bool, bulk: bool, enable_only: bool, disable_only: bool, skip_snapshot: bool, include_version: bool, ) -> None: snapshot = read_snapshot_file(name) snapshot_plugins = set(snapshot.get("plugins", [])) print(f"Snapshot '{name}' ({snapshot.get('created', 'unknown')})") print(f" {len(snapshot_plugins)} plugin(s) enabled in snapshot") if snapshot.get("plugin_versions"): print(f" Plugin versions recorded: {len(snapshot['plugin_versions'])}") print() print("Fetching plugin list...") all_plugins = plugins_obj.get_all_plugins() if all_plugins and "error" in all_plugins[0]: print(f"Failed to fetch plugins: {all_plugins[0]['error']}") sys.exit(1) print(f" {len(all_plugins)} plugins total\n") by_folder = {p["folderName"]: p for p in all_plugins} to_enable: list = [] if disable_only else sorted( f for f in snapshot_plugins if f in by_folder and not by_folder[f].get("isEnabled") ) to_disable: list = [] if enable_only else sorted( p["folderName"] for p in all_plugins if p.get("isEnabled") and p["folderName"] not in snapshot_plugins ) not_in_instance = sorted(f for f in snapshot_plugins if f not in by_folder) already_correct = max(0, len(snapshot_plugins) - len(to_enable) - len(not_in_instance)) _print_update_plan(to_enable, to_disable, already_correct) if dry_run: _print_dry_run_preview(to_enable, to_disable) if not_in_instance: print(f"\nNot found in instance ({len(not_in_instance)}):") for f in not_in_instance: print(f" - {f}") return if not skip_snapshot and (to_enable or to_disable): _auto_save_snapshot(all_plugins, include_version) errors = _apply_disable(plugins_obj, to_disable, bulk) + _apply_enable(plugins_obj, to_enable, bulk) if not to_enable and not to_disable: print("Nothing to change.") else: print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {errors}") if not_in_instance: print(f"\nNot found in instance ({len(not_in_instance)}):") for f in not_in_instance: print(f" - {f}") # Main def main() -> None: import argparse parser = argparse.ArgumentParser( prog="plugin_manager_pysisense.py", description="Manage Sisense plugin states via pySisense.", 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." ), ) parser.add_argument( "--config", default=str(DEFAULT_CONFIG), 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_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_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() if args.list_snapshots: cmd_list_snapshots() return cfg = load_config(Path(args.config)) sisense_cfg = cfg.get("sisense", {}) bulk = sisense_cfg.get("bulk", True) and not args.no_bulk client = build_client(sisense_cfg) plugins_obj = Plugins(api_client=client) if args.enable_one: cmd_enable_one( plugins_obj, args.enable_one, dry_run=args.dry_run, bulk=bulk, skip_snapshot=args.skip_snapshot, include_version=args.log_version, ) elif args.save_snapshot is not None: name = ( datetime.now().strftime("snapshot_%Y%m%d_%H%M%S") if args.save_snapshot == "__auto__" else args.save_snapshot ) cmd_save_named_snapshot(plugins_obj, name, args.log_version) elif args.restore_snapshot is not None: name = ( latest_snapshot_name() if args.restore_snapshot == "__latest__" else args.restore_snapshot ) cmd_restore_snapshot( plugins_obj, name, dry_run=args.dry_run, bulk=bulk, enable_only=args.enable_only, disable_only=args.disable_only, skip_snapshot=args.skip_snapshot, include_version=args.log_version, ) else: enable_names = cfg.get("enable_plugins") or cfg.get("plugins", []) disable_names = cfg.get("disable_plugins", []) 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( plugins_obj, enable_names, disable_names, dry_run=args.dry_run, bulk=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() Disclaimer: This post outlines a potential custom workaround for a specific use case or provides instructions regarding a specific task. The solution may not work in all scenarios or Sisense versions, so we strongly recommend testing it in your environment before deployment. If you need further assistance with this, please let us know.

      Jeremy Friedel
      Jeremy FriedelPosted 1 month ago • Last reply 1 month ago
      1
               
    • 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
               
      • TroubleshootingChevronRightIcon

      JumpToDashboard - Troubleshooting the most common configuration issues

                               

      JumpToDashboard - Troubleshooting the most common configuration issues This article provides possible solutions to the most common configuration issues with the JumpToDashboard plugin.  Please review the symptom of the issue first (what error/behavior you experience with the JumpToDashboard plugin) and then follow the solution instructions . If this doesn’t solve your issue, feel free to contact our Support Team describing your issue in detail. Symptoms: A target (usually with “_drill” in prefix) dashboard disappeared for non-owner users in the left-hand side panel. Solution: this behavior could be intended and is controlled by the JumpToDashboard parameter called “ hideDrilledDashboards ”. To make the dashboard visible for the non-owners, please check the following: 1. Log in as a dashboard owner and find the dashboard in question in the left-hand side panel. Click the 3-dots menu and make sure it’s not hidden: 2. If it’s not hidden by the owner intentionally, then navigate to the Admin tab > System Management (under Server & Hardware) > File Management > plugins > jumpToDashboard > js > config.js and check if hideDrilledDashboards set to true. If so, then change it to false and save the changes in the config file. 3. Wait until the JumpToDashboard plugin is rebuilt under the Admin tab > Server & Hardware > Add-ons page and ask your user to refresh the browser page to check if a drill dashboard appears on the left-hand side panel. Symptoms: No "Jump to dashboard" menu appears in a widget edit mode clicking the 3-dots menu.  Solution: there could be different reasons for such behavior so check the most common cases below: Double-check if the JumpToDashboard plugin is enabled under the Admin tab > Server & Hardware > Add-ons page.  Make sure that both dashboards (parent and target) are based on the same ElastiCube. By default, the JumpToDashboard plugin has sameCubeRestriction : true in the config.js file that prevents the ‘jump to’ menu from appearing when a drill dashboard uses a different data source. Check that the prefix you used for the drill dashboard creation is correct. It could be changed in the config.js file. By default, it uses “_drill”: Symptoms: when clicking on a widget that should open a drill dashboard, nothing happens.  Solution : in such cases, we recommend opening your browser console (for example, F12 for Chrome > Console tab) to see if there are any errors that could indicate the issue.  For example, a 403 error in the console indicates that the target dashboard is not shared with the user who is experiencing the issue. To fix it, login as an owner of the drill dashboard and share it with the relevant user or group. Symptoms: when clicking on a widget to get the drill dashboard you get a 404 error. Solution: This issue usually happens when the target/drill dashboard is removed from the system. In order to fix it, please follow the steps below: Log in to the system as an owner. Find the parent widget and open it in edit mode.  Click the 3 dots menu > choose the ‘Jump to dashboard’ menu and select any other dashboard that exists in the system. Press Apply and publish the changes to other users.  Note: if you need just to remove a drill dashboard that doesn’t exist from this widget and not substitute it with another one, try the following: a fter you choose a new drill dashboard, just unselect it after that and then save the changes.  If the jump to dashboard menu doesn’t appear for this widget, try to create a new temporary dashboard with “_drill” in the prefix and do the same.   Symptoms:  The drill dashboard is not opening for some viewers. Solution:  republish the drill dashboard to make sure the updated version is delivered to all end users. Additional Resources: JumpToDashboard Plugin - How to Use and Customize

      Liliia Kislitsyna
      Liliia KislitsynaPosted 2 years ago • Last reply 11 months ago
      2
               
      • How-Tos & FAQsChevronRightIcon

      Changing Grand Total Position

               

      Question: In pivot tables, we would like to show column grand totals on top of the table and row grand totals at front. Please let us know how to alter positions. Asnwer: This script will move the grand total row to the top.   widget.on('ready', function(sender, ev){ if ($(".p-grand-total-row-head", element).length > 0) { setTimeout(function(){ $('table#pivot_',element).prepend($('table#pivot_', element).find('tr:last')); $('div.table-container table', element).prepend($('div.table-container table', element).find('tr:last')); $('tbody',element).each(function(){ $(this).find('tr:first td').not('.phantom').css('border-bottom', '1px solid #ccc').find($('span, .p-value')).css('font-weight', 'bold'); }); $('.p-grand-total-row-head.p-first-data-col').css('border-left', 'none'); },500); } }); The below should work with Pivot 2.0.  widget.on('ready', function(sender, ev){ //Define Variables var lastRowClass = $('tr.table-grid__row:contains(Grand Total)',element).last().attr('class') var firstClass = lastRowClass.split(' ')[0] var secondClass = lastRowClass.split(' ')[1] var firstRowClassBase = $('div.' + firstClass).first().attr('class').split(' ')[0] var firstRowClassNumber = $('div.' + firstClass).first().attr('class').split(' ')[1].match(/\d+/g) //If the first row class number does NOT equal zero... if(firstRowClassNumber[0] != 0){ $('div.' + firstRowClassBase + ' div.table-grid__cell').css({'border-top':'','font-weight':''}) $('tr.' + firstClass).first().insertAfter($('tr.table-grid__row-' + (firstRowClassNumber - 1))) $('div.' + firstClass).first().insertAfter($('div.table-grid__row-' + (firstRowClassNumber - 1))) } //Move Grand Total row to top $('div.' + secondClass).insertBefore($('div.' + firstClass).first()) $('tr.' + secondClass).insertBefore($('tr.' + firstClass).first()) $('div.' + secondClass + ' div.table-grid__cell').css({'border-bottom':'1px solid #ccc','border-top':'1px','font-weight':'bold'}) $('tr.' + secondClass + ' td').css({'border-bottom':'1px solid #ccc','border-top':'1px','font-weight':'bold'}) });

      intapiuser
      intapiuserPosted 3 years ago • Last reply 1 year ago
      2
               
      • Add-ons & Plug-InsChevronRightIcon

      Add Javascript To Every Dashboard

               

      Question: Has anyone written a plugin to add some Javascript to every dashboard page or is there an easier way to do it? For example, we are going to start using SaleMove and I have been asked to add a Javascript on every dashboard. Rather than editing the script on each dashboard manually, I would like to just automatically have it generate on each page.   Answer: In fact, any Sisense plugin runs in a global scope - it is executed once when the Sisense UI loads and can apply to the entire application, so code targeted at dashboards can apply to all dashboards. Next, to apply an operation to each dashboard that is opened by the user, you can use the global event "dashboardloaded" as is described in our  developer documentation website / javascript API Using this event, I could for example implement a very simple plugin that reports to an external usage tracker every time a user opens a dashboard, like so: // registering the event handler prism.on("dashboardloaded", sendTrackingInfo); // handler implementation function sendTrackingInfo(dashboard){ var payload = { user: prism.user.email, dashboardId: dashboard.oid, dashboardName: dashboard.title }; $.post("https://myimaginarytracker.com/report/someImaginaryToken1234/", JSON.stringify(payload)); }

      intapiuser
      intapiuserPosted 3 years ago
      0
               
      • Add-ons & Plug-InsChevronRightIcon

      Create A New Plugin

               

      INTRODUCTION  Sisense has a very rich and robust plugin framework that allows users to extend the core functionality beyond what is available with the out-of-the-box Widget. There are many ways to extend Sisense functionality; however, in this document will focus on creating a new plugin via the plugin framework. This document provides an overview on how to create a Custom Plugin using a JavaScript plugin framework.  Plugin Location Folder Sisense stores all custom plugin in the following folder  C:\Program Files\Sisense\PrismWeb\plugins  . Each customer plugin will be placed in a separate directory that will include at minimum the following files: json js css Widget icon file in PNG format   GETTING STARTED The best way to get started, is to copy the files from an existing custom plugin folder to a new directory and edit the properties of each file based on your custom requirements. Plugin.json This file acts as the main project file, which will have information file included within this project. Normally this will be widget.js, but other files can be added such as external libraries or third party code. This file will also include references to the plugin stylesheet file Widget.css.  The below example shows what the Plugin.json file would look like. Notice that “widget.js” and “widget.css” must be there by default. The other files (liquidFillGauge.js, stylerController.js) are additional library that the plugin will reference.   Widget.js This is the main plugin file where all the magic happens. The file has several sections that will need to be customized in order for the plugin to work. The sections that need to be customized are the following: Plugin general properties Plugin input parameters (also called Panels) Plugin Query Processing Results Render     1. Plugin General Properties Name  - the internal name of the plugin within the Sisense framework to be used to register the plugin. There are 2 places that will need to be modified as seen below:   Family  - indicates the family type for this plugin and generally can be any text. Some examples for family types are : Indicator, tree, chart, pivot, etc. See below example:   Title  - displays the title of the plugin when user is making selection from the Dashboard designer. See below example:   iconSmall - points to the location of the plugin icon. This should be a PNG file of size 24x48 to hold the 2 24x24 icons of the plugin (selected and unselected), see below examples of an iconSmall file: 2. Plugin Input Parameter The plugin input parameter, also known as panel, is where user selects which fields or formula to source the data from. The plugin has a “panel” object that is an array of all the input parameters. In the below example the plugin has implemented 3 parameters (Value, Min, and Max). The below illustrates the code for declaring the “Value” panel and what it would look like in the design mode: Panel Properties Name  - name of the input parameter Type  - should be set to “visible” itemAttributes - define the option the user will have to set any special formatting attribute to a specific panel such as number or color formatting Metadata  - defines the type of value that is allowed for this parameter. It can be measures or dimensions. Visibility  - set to true to make this parameter visible by default.   3. Plugin Query (buildQuery) This section will push all the plugin parameters, as well as any filters into the Plugin JAQL object. This query will be send to Sisense server for execution and will be returned in the ProcessResult event as described below. In general, the below function will create a query object, and will push all the panels values into it as seen below. The below code will work for most cases, and you just need to modify the panel name accordingly: 4. Processing Result (processResult) This event will get fired after the above query was executed by Sisense server allowing the user to process the results of the query and create the queryResult object that is more suitable for processing. Sisense will return a dataset that is in a tabular format based on the input parameter that is accessible via the $$rows object. Each row item will contain values corresponded to the input parameters. In the below example, the result set will only have one value (i.e. number of rows will always be 1), so we only need to loop through all the input parameters that have value and fetch the value from the first row accordingly: Once we have created our own result set, we must return this object before exiting the function. This will be saved to the widget.queryResult object and will used when rendering the plugin display.   5. Render This event is called after the processResult event (as described above). In this event we will have the chance to output any visual to the user on the plugin display panel. This can be a simple textbox or a table, or more advanced visualization using D3 as an example to plot the dataset. We would need to access the plugin output display and this is possible using the below code where we get access to the element property:   Next, we would want to clear any previous display by calling the element.empty() method as seen below. This will result in having a blank canvas ready to be refreshed with our new dataset. We can also fetch some basic properties of our canvas such as height and width, that can be used to align our drawing to the plugin canvas size. In most cases we would need to add a new SVG element to our canvas using the element.append function as seen below: This will create a new container in the plugin canvas which we can use to draw any visualization as seen below: To summarize, this document covers the steps needed to create a basic custom plugin within Sisense. This document does not cover any of the advanced techniques, such as creating design options, custom queries, and other. Once you have a fully functional plugin, it’s time to share your work with others in the Sisense community. 

      intapiuser
      intapiuserPosted 3 years ago
      0
               
      • How-Tos & FAQsChevronRightIcon

      Hide Value From Tool-Tip

               

      Question: I am looking for a way to hide the latitude/longitude from the tool-tip in a scatter map. It appears by default and I searched through other posts but couldn't find anything. I still want the tool tip to appear, just remove the lat/long.   Answer: The lat/long are displayed because they are the marker's name - when a textual field is used (such as a country field) - that would be displayed in place of lat/long. There are two possible ways of achieving this: By clearing the value of the marker name By providing a custom HTML template without it In both cases, you would start with the widget event "beforedatapointtooltip" as described in the  Sisense Javascript API Reference widget.on("beforedatapointtooltip", (event, params) => { // params contains: widget, context, template, cancel }); For approach #1  you can set the marker name to nothing: params.context.marker.name = ""; For approach #2  find the original template in the path: C:\Program Files\Sisense\PrismWeb\client\views\scattermap\scattermap-tooltip.html Then copy it into a plugin, edit to remove the HTML element referring to the "marker.name" property, and then change the "template" to point to your custom template: params.template = '<div data-ng-include="../plugins/myCustomTemplatePlugin/custom-scattermap-tooltip.html"></div>'; You could also simply paste the HTML itself as the "template" but that approach is less maintainable in the long run, so using a template file is preferable.

      intapiuser
      intapiuserPosted 3 years ago
      0