Category: Aether

Aether

  • Creating Card Slicers in PowerBI

    Creating Card Slicers in PowerBI

    One of the things I hear the most when running demos or working with customers.

    “If I click on that KPI will that then filter the report”

    Sadly, no. Or well not by default. Kick in Product Manager brain, make it work!

    So in this blog post I will be running through my implementation of creating cards that can do exactly that, be clicked on, filter the rest of the report and fit nicely with the overall aesthetic I’ve built with Prism. We can also throw in some SVG animation to enhance everything visually and give it that extra level of polish.

    So, how do we make KPI cards clickable?

    Power BI doesn’t natively allow KPIs to act as slicers, so we need a workaround.

    To be able to click and filter in PowerBI we have a few slicer options, but to make it look like a KPI card that’s when we need to consider some options:

    We could:

    • Layer it (create a card and then add a dummy button slicer on top)
    • Use a Button Slicer

    In this example, I’ve gone with the button slicer approach. It integrates cleanly into the overall product UI and is easier to manage as I evolve the design further.

    The trick: SVG + Measures = Interactive KPI

    In some previous posts, Ive used HTML / SVG with measures that we can then add to the image url field. Ive used the same logic here but then also added a state for Static, Hover and Selected. As we also need to think about making it obvious for end users to see we’ve added a filter.

    Some subtle icons and animation can be really effective here

    Tutorial

    To start off:

    • Create a button slicer
    • Add the 3 different measures below

    Below are my SVG measures. Each one displays a simple KPI-style card with a filter icon and adjusts styling such as colour and animation based on state (Static, Hover, Selected). The end result looks something like this:

    HTML_Card_Filter = 
    
    VAR pct = COUNTROWS(
        FILTER(
            Sheet1,
            Sheet1[allocationfilter] = 1
        )
    )
    VAR sizeWidth = 300
    VAR sizeHeight = 110
    
    -- Colors
    VAR baseColor = "#373737"
    VAR textColor = "#f5f5f5"
    
    -- SVG generation
    VAR svg = "
    <svg xmlns='http://www.w3.org/2000/svg' width='" & sizeWidth & "' height='" & sizeHeight & "' viewBox='0 0 " & sizeWidth & " " & sizeHeight & "'>
      <style>
        .card-base {
          fill: " & baseColor & ";
        }
        .accent {
          fill: #6BFAD8;
        }
        .title {
          font-family: Segoe UI, sans-serif;
          font-size: 14px;
          font-weight: 600;
          fill: " & textColor & ";
          text-anchor: end;
        }
        .value {
          font-family: Segoe UI, sans-serif;
          font-size: 42px;
          font-weight: 600;
          fill: " & textColor & ";
          text-anchor: end;
        }
      </style>
    
      <!-- Background -->
      <rect class='card-base' width='" & sizeWidth & "' height='" & sizeHeight & "' rx='10' ry='10'/>
      
      <!-- Right accent -->
      <path class='accent' d='M " & (sizeWidth - 5) & " 0 L " & (sizeWidth - 5) & " " & sizeHeight & " Q " & sizeWidth & " " & sizeHeight & " " & sizeWidth & " " & (sizeHeight - 10) & " L " & sizeWidth & " 10 Q " & sizeWidth & " 0 " & (sizeWidth - 5) & " 0 Z'/>
      
      <!-- Filter icon in top left -->
      <g transform='translate(18, 18)'>
        <path d='M2 3h12l-4 4v4l-4 2V7L2 3z' stroke='" & textColor & "' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/>
      </g>
    
      <!-- Text -->
      <text x='" & (sizeWidth - 30) & "' y='60' class='value'>" & pct & "</text>
      <text x='" & (sizeWidth - 30) & "' y='80' class='title'>&lt;50% Allocated</text>
    </svg>
    "
    
    RETURN "data:image/svg+xml;utf8," & svg
    
    HTML_Card_Filter_Hover = 
    
    VAR pct = COUNTROWS(
        FILTER(
            Sheet1,
            Sheet1[allocationfilter] = 1
        )
    )
    VAR sizeWidth = 300
    VAR sizeHeight = 110
    VAR animationKey = CONCATENATE("anim-", FORMAT(NOW(), "hhmmss"))
    
    -- Colors
    VAR baseColor = "#373737"
    VAR textColor = "#f5f5f5"
    
    -- SVG generation
    VAR svg = "
    <svg xmlns='http://www.w3.org/2000/svg' width='" & sizeWidth & "' height='" & sizeHeight & "' viewBox='0 0 " & sizeWidth & " " & sizeHeight & "'>
      <style>
        .card-base {
          fill: " & baseColor & ";
        }
        .accent {
          fill: #6BFAD8;
        }
        .title {
          font-family: Segoe UI, sans-serif;
          font-size: 14px;
          font-weight: 600;
          fill: " & textColor & ";
          text-anchor: end;
        }
        .value {
          font-family: Segoe UI, sans-serif;
          font-size: 42px;
          font-weight: 600;
          fill: " & textColor & ";
          text-anchor: end;
        }
        .animated-rect {
          fill: #6BFAD8;
          opacity: 0.7;
        }
      </style>
    
      <!-- Background -->
      <rect class='card-base' width='" & sizeWidth & "' height='" & sizeHeight & "' rx='10' ry='10'/>
      
      <!-- Right accent -->
      <path class='accent' d='M " & (sizeWidth - 5) & " 0 L " & (sizeWidth - 5) & " " & sizeHeight & " Q " & sizeWidth & " " & sizeHeight & " " & sizeWidth & " " & (sizeHeight - 10) & " L " & sizeWidth & " 10 Q " & sizeWidth & " 0 " & (sizeWidth - 5) & " 0 Z'/>
      
      <!-- Filter icon in top left -->
      <g transform='translate(18, 18)'>
        <!-- Static rectangle over filter icon -->
        <rect x='2' y='16' width='12' height='4' rx='2' class='animated-rect' data-anim='" & animationKey & "'/>
        <path d='M2 3h12l-4 4v4l-4 2V7L2 3z' stroke='" & textColor & "' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/>
      </g>
    
      <!-- Text -->
      <text x='" & (sizeWidth - 30) & "' y='60' class='value'>" & pct & "</text>
    <text x='" & (sizeWidth - 30) & "' y='80' class='title'>&lt;50% Allocated</text>
    </svg>
    "
    
    RETURN "data:image/svg+xml;utf8," & svg
    
    
    HTML_Card_Filter_Selected = 
    
    VAR pct = COUNTROWS(
        FILTER(
            Sheet1,
            Sheet1[allocationfilter] = 1
        )
    )
    VAR sizeWidth = 300
    VAR sizeHeight = 110
    VAR animationKey = CONCATENATE("anim-", FORMAT(NOW(), "hhmmss"))
    
    -- Colors
    VAR baseColor = "#373737"
    VAR textColor = "#f5f5f5"
    
    -- SVG generation
    VAR svg = "
    <svg xmlns='http://www.w3.org/2000/svg' width='" & sizeWidth & "' height='" & sizeHeight & "' viewBox='0 0 " & sizeWidth & " " & sizeHeight & "'>
      <style>
        .card-base {
          fill: " & baseColor & ";
        }
        .accent {
          fill: #6BFAD8;
        }
        .title {
          font-family: Segoe UI, sans-serif;
          font-size: 14px;
          font-weight: 600;
          fill: " & textColor & ";
          text-anchor: end;
        }
        .value {
          font-family: Segoe UI, sans-serif;
          font-size: 42px;
          font-weight: 600;
          fill: " & textColor & ";
          text-anchor: end;
        }
        .animated-rect {
          fill: #6BFAD8;
          opacity: 0.9;
        }
        .pulse-glow {
          fill: none;
          stroke: rgba(255, 255, 255, 0.8);
          stroke-width: 1;
          opacity: 0;
        }
      </style>
    
      <!-- Background -->
      <rect class='card-base' width='" & sizeWidth & "' height='" & sizeHeight & "' rx='10' ry='10'/>
      <!-- Right accent -->
      <path class='accent' d='M " & (sizeWidth - 5) & " 0 L " & (sizeWidth - 5) & " " & sizeHeight & " Q " & sizeWidth & " " & sizeHeight & " " & sizeWidth & " " & (sizeHeight - 10) & " L " & sizeWidth & " 10 Q " & sizeWidth & " 0 " & (sizeWidth - 5) & " 0 Z'/>
      
      
      <!-- Filter icon in top left -->
      <g transform='translate(18, 18)'>
        <!-- Pulsing rectangle over filter icon -->
        <rect x='2' y='16' width='12' height='4' rx='2' class='animated-rect'>
          <animate attributeName='opacity' values='0.9;0.1;0.9' dur='1.5s' repeatCount='indefinite' begin='0s'/>
          <animate attributeName='fill' values='#6BFAD8;#4ECDC4;#6BFAD8' dur='1.5s' repeatCount='indefinite' begin='0s'/>
        </rect>
        <path d='M2 3h12l-4 4v4l-4 2V7L2 3z' stroke='#6BFAD8' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/>
      </g>
    
      <!-- Text -->
      <text x='" & (sizeWidth - 30) & "' y='60' class='value'>" & pct & "</text>
    <text x='" & (sizeWidth - 30) & "' y='80' class='title'>&lt;50% Allocated</text>
    </svg>
    "
    
    RETURN "data:image/svg+xml;utf8," & svg
    

    Once you have added your button slicer and added the above measures, we need to apply them to the different states and ensure all looks and works as required.

    In my example, I used a calculated column linked to my table that was simply filtered on a 1 or 0 depending on the conditions – then linked to the wider data. (The data I used was looking at allocation rates, over 50% was a 1 everything else a 0)

    Next:

    • On the filters if required set the filter on that visual to show what you are looking to filter on (i.e “1”)
    • Under layout, we need to show only the 1 row and column and I generally set the space between buttons as 0

    Now to prepare for using the image measures we need to hide everything else

    Under General

    • Disable Title
    • Disable Header Icons
    • Disable Effects / Background

    Under Visual / Buttons

    • Disable Border
    • Disable Full
    • Padding – Set to custom and set all to 0 px

    Under Callout Values

    • Disable Values

    You should now essentially have a box that shows nothing!

    Now back under visual / Image we need to apply the 3 measures – apply the following based on the state

    AllHoverSelected
    HTML_Card_FilterHTML_Card_HoverHTML_Card_Selected

    With all those added you should now have you Card Slicer!

    Now you will need to use fields and titles that make sense to your data, those can all be changed within the measures themselves, just dont forget to change in all 3!

    Once complete you should end up with something like the following:

    Why This Matters

    Beyond just looking nice, this pattern supports:

    • Design flexibility: Themed, responsive KPIs tailored to your report’s look and feel
    • Better UX: Users get clear, visual feedback
    • Faster interactions: No more guessing what’s clickable

    Thanks for reading! As always I would love to hear from you, as well as any ideas you have!

  • Creating SVG Powered Navigation Buttons in PowerBI

    Creating SVG Powered Navigation Buttons in PowerBI

    Not every SVG needs to be used as a measure in Power BI. In some cases, it makes more sense to take a simpler approach and use .SVG files directly. This is especially helpful when working with buttons in Power BI, which currently do not support imageURL (like we would use in the new card visuals, for example) and instead only allow image input. Hopefully this blog post helps you with creating SVG powered navigation buttons in PowerBI.

    Thankfully, those image options give us all we need to make buttons more dynamic. You can use animated SVG icons or even animate the entire button if needed.

    Power BI buttons let you define multiple visual states, including Default, Hover, Selected etc. Each state can be customised with different settings for Text, Icon, Fill, and Border. Both Icon and Fill accept image files.

    SVG files can contain coded information, which means you can embed animation directly into them. You just need to make sure the .SVG file is correctly structured and works as expected inside Power BI.

    A useful tool for testing and previewing SVG files is https://www.svgviewer.dev.

    This example uses the Icon section of the button visual. Here, we apply an animated SVG that responds to the button state, such as when a user hovers over it.

    For this example, I am creating a simple arrow that moves up and down when the button is hovered over. We need two versions of the SVG. One will be static for the default state. The other will contain the animation for the hover state.

    Note: Make sure to include the namespace in your SVG. This is required for it to render correctly in Power BI.

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <svg 
       xmlns="http://www.w3.org/2000/svg"
    width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
      <line x1="6" y1="18" x2="18" y2="6" />
      <polyline points="10 6 18 6 18 14" />
    </svg>
    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <svg 
       xmlns="http://www.w3.org/2000/svg"
    width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
      <g>
        <animateTransform
          attributeName="transform"
          type="translate"
          values="0 0; 1 -1; 2 -2; 3 -3; 4 -4; 0 0"
          keyTimes="0; 0.1; 0.25; 0.45; 0.8; 1"
          dur="2s"
          repeatCount="indefinite"
        />
        <line x1="6" y1="18" x2="18" y2="6" />
        <polyline points="10 6 18 6 18 14" />
      </g>
    </svg>

    Copy the code up to https://www.svgviewer.dev/ to preview, from here you can also download the .svg file as well which makes things nice and simple!

    Into PowerBI, Insert a new blank button, select the button to format at and under Style / Icon, choose Icon Type – Custom

    Next we need to apply the two image files, one for the default status and the other for hover

    Default

    • Under Style Apply Settings to: Firstly make the state as Default
    • Next, go back to the Icon section and apply the static SVG. I set the image fit as normal
    • Tweak the placement as required using alignment and padding ( I used left horizontal alignment, middle vertical, with padding of 10 on the left and 7 at the bottom.
    • I then set the icon size at 40, but against this can be tweaked as required

    Hover

    • This time under Style Apply Settings to: Switch to Hover
    • Back Under Icon, apply the animated svg. And set the image fit the same as your default setting (especially if using a similar icon for each)
    • If using the same icon like I have in this example ensure your placement and size matches the default settings
    • In my example I also added an extra border around the button, feel free to experiment and see what you like best!

    This example instead uses the Fill option of the button visual. Again, we apply an animated SVG that responds to the button state, such as when a user hovers over it but this time instead of affecting an icon, we can affect the button itself.

    For this example, a wave animation down the left hand side of the button that appears once hovered over. Unlike the icon example, we only need one SVG file this time as our default state for this example is just a solid fill background (just make sure your background matches the colour we are adding for the svg)

    Note: Make sure to include the namespace in your SVG. This is required for it to render correctly in Power BI.

    <svg xmlns="http://www.w3.org/2000/svg" width="259" height="74" viewBox="0 0 259 74">
      <style>
        .wave-accent {
          fill: #6bfad8;
          animation: waveMove 4s linear infinite;
        }
    
        @keyframes waveMove {
          0% { transform: translateY(0); }
          100% { transform: translateY(-160px); }
        }
      </style>
      <rect x="0" y="0" width="259" height="74" rx="6" ry="6" fill="#292929" />
      <defs>
        <clipPath id="waveClip">
          <rect x="0" y="0" width="25" height="74" />
        </clipPath>
      </defs>
      <g clip-path="url(#waveClip)">
        <g class="wave-accent">
          <path d="M12.5,0 q10,10 0,20 -10,10 0,20 10,10 0,20 -10,10 0,20 10,10 0,20 -10,10 0,20 10,10 0,20 -10,10 0,20 H0 V0 Z" />
          <path d="M12.5,160 q10,10 0,20 -10,10 0,20 10,10 0,20 -10,10 0,20 10,10 0,20 -10,10 0,20 10,10 0,20 -10,10 0,20 H0 V160 Z" />
        </g>
      </g>
    </svg>
    

    Copy the code up to https://www.svgviewer.dev/ to preview and download the .SVG file.

    Note this time, we have added the width and height in various parts of the code. This needs to match up with your size of the button in PowerBI to ensure if fills the whole area

    Into PowerBI, Insert a new blank button, select the button to format at and under Style / Icon, choose Icon Type – Custom

    Next we need to apply the single image file for the hover status

    Default

    • Under Style Apply Settings to: Ensure we are set on Default
    • Ensure the fill colour is set to exactly the same colour being used within your button
    • You may want to tweak the positioning of the text on your button, I applied middle horizontal and vertical alignment and then moved it around with padding

    Hover

    • Under Style Apply Settings to: Switch to Hover
    • Under Fill, browse and apply the animated svg. In this instance I’ve set the Image Fit just as Normal

    And that now should be it! You should have two different types of animated navigation buttons, you can apply the actions you need them to do down in the button settings and you are sorted!

    Please play around experiment with different types and shapes and of course share!

    Thanks for reading!

  • Updating PowerBI reports with Powershell Version 2

    Updating PowerBI reports with Powershell Version 2

    After a recent update for Prism, I needed to make some changes to my PowerBI update script. This is updating PowerBI reports with Powershell version 2!

    My Powershell scripts have served me well for some time, but after a couple of errors popped up, I found a few extra ways to make it more efficient and also improve how it was handling parameters!

    One thing I noticed was that parameters occasionally failed to load correctly after a PBIX upload. This was due to the upload automatically triggering a dataset refresh, which temporarily blocked the API from accepting new parameter values.

    Turns out this was due to having scheduled refreshes enabled on the existing dataset. The solution, temporarily disable the scheduled refresh!

    So if you have the requirement where you have multiple instances of the same report across the same or different workspaces and need to update them with your latest pbix file this script is for you!

    What the script does?

    This script will pick up a pbix file then scan over your workspaces (based on when filters you apply). When it finds reports that again match your criteria, it will take a copy of the current parameters, temporarily disable the scheduled refresh, update the pbix file and then load the parameters back in. When its finished, it will reenable the scheduled refresh. In some cases I’ve then set it to perform a refresh straight after to load the data back into the report.

    So this means you can upload to the PowerBI service with your pbix file from a single source file, the parameters all remain the same as before, and then we pull in the data based on those parameters and all good to go!

    As part of version control, I currently use a parameter to record the current version of my reports. This adds an extra benefit, as I can then use this script to check if the reports are already on the expected version. Just in case the script is interrupted, or we need to deploy in stages.

    I then add in the new version number as part of the script and this becomes a great way to track report versions when updating PowerBI reports with PowerShell.

    How it works?

    • Connects to Power BI Service using Connect-PowerBIServiceAccount.
    • Retrieves all Power BI workspaces using Get-PowerBIWorkspace.
    • Loops through each workspace and identifies reports whose names start with “AETHER”. (or however best for you)
    • For each report:
    • Retrieves the dataset parameters.
    • Updates the first parameter’s value to a specified version if it doesn’t already match.
    • Disables scheduled refresh
    • Overwrites the report in the workspace using a specified PBIX file ($FilePath).
    • Takes over the dataset to ensure permissions are set correctly.
    • Updates the dataset parameters.
    • Re-enables the scheduled refresh

    Key things you can change

    • $DeployVer: The new deployment version to update the first parameter to (e.g., “2025Q2”).
    • The reports to find matching a name convention e.g. $_.Name -LIKE ‘*AETHER*’
    • $FilePath: The path to the PBIX file used for updating reports.
    • Parameter 0 is assumed to represent a version parameter (e.g., “2025Q2”)
    # Connect to Power BI Service Account
    Connect-PowerBIServiceAccount
    
    # Set the deployment version to be used for updating parameters
    $DeployVer = "2025Q2"
    
    # Define the path to the PBIX file to be used for report updates
    $FilePath = "C:\MYFILEPATH\REPORT.pbix"
    
    # Define the conflict action for updating reports (e.g., Create or Overwrite existing reports)
    $Conflict = "CreateOrOverwrite"
    
    # Retrieve all Power BI workspaces
    $workspaces = Get-PowerBIWorkspace -all
    
    # Loop through each workspace
    foreach ($workspace in $workspaces) {
    
        # Get all reports in the current workspace with names starting with "AETHER" - adjust the filter as needed
        $Reportlist = Get-PowerBIReport -WorkspaceId $workspace.Id | Where-Object -FilterScript {
            $_.Name -LIKE '*AETHER*'
        }
    
        # Check if any reports were found in the workspace
        if ($Reportlist) {
            Write-Host "Workspace: $($workspace.Name)" # Log the workspace name
    
            # Loop through each report in the report list
            foreach ($Report in $Reportlist) {
                Write-Host "  Report: $($Report.Name)" # Log the report name
    
                try {
                    # Retrieve the parameters of the dataset associated with the report
                    $ParametersJsonString = Invoke-PowerBIRestMethod -Url "https://api.powerbi.com/v1.0/myorg/groups/$($workspace.Id)/datasets/$($Report.DatasetId)/parameters" -Method Get
                    $Parameters = (ConvertFrom-Json $ParametersJsonString).value # Convert JSON response to PowerShell object
                } catch {
                    Write-Host "Error retrieving parameters: $($_.Exception.Message)"
                    continue
                }
    
                $JsonBase = @{}
                $JsonString = $null # Initialize JSON string variable
    
                # Initialize an empty array to hold parameter updates
                $UpdateParameterList = New-Object System.Collections.ArrayList
    
                # Loop through each parameter and prepare the update list
                foreach ($Parameter in $Parameters) {
                    $UpdateParameterList.add(@{"name" = $Parameter.name; "newValue" = $Parameter.currentValue})
                }
    
                # Check if there are any parameters to update
                if ($UpdateParameterList.Count -gt 0) {
                    # Get the current value of the Version parameter
                    $currentparam = $UpdateParameterList[0].newValue
    
                    Write-Host "Current Parameter Version Value: $currentparam" # Log the current parameter value
    
                    # Check if the current parameter value matches the deployment version
                    if ($currentparam -ne $DeployVer) {
                        Write-Host "Version does not match. Updating..." # Log the update action
    
                        # Display current parameters
                        $UpdateParameterList.newValue
    
                        # Update the first parameter to the new deployment version
                        $UpdateParameterList[0].newValue = $DeployVer
    
                        # Prepare the JSON payload for updating parameters
                        $JsonBase.Add("updateDetails", $UpdateParameterList)
                        $JsonString = $JsonBase | ConvertTo-Json
    
                        # Define the report name
                        $ReportName = $Report.Name
    
                        # Disable refresh schedule for the dataset
                        $disableRefreshBody = @"
    {
    "value": {"enabled": false}
    }
    "@
    
                        try {
                            Invoke-PowerBIRestMethod -Url "https://api.powerbi.com/v1.0/myorg/groups/$($workspace.Id)/datasets/$($Report.DatasetId)/refreshSchedule" -Method Patch -Body ("$disableRefreshBody")
                            Write-Host "Refresh schedule disabled for dataset: $($Report.DatasetId)"
                        } catch {
                            Write-Host "Failed to disable refresh schedule: $($_.Exception.Message)"
                        }
    
                        try {
                            # Take over the dataset to ensure permissions are set correctly
                            Invoke-PowerBIRestMethod -Url "https://api.powerbi.com/v1.0/myorg/groups/$($workspace.Id)/datasets/$($Report.DatasetId)/Default.TakeOver" -Method Post
                        } catch {
                            Write-Host "Error taking over dataset: $($_.Exception.Message)"
                            continue
                        }
    
                        try {
                            # Update the existing report in the workspace
                            New-PowerBIReport -Path $FilePath -Name $ReportName -WorkspaceId $workspace.Id -ConflictAction $Conflict
                        } catch {
                            Write-Host "Error uploading report: $($_.Exception.Message)"
                            continue
                        }
    
                        try {
                            # Update the parameters of the dataset
                            Start-Sleep 5
                            Invoke-PowerBIRestMethod -Url "https://api.powerbi.com/v1.0/myorg/groups/$($workspace.Id)/datasets/$($Report.DatasetId)/Default.UpdateParameters" -Method Post -Body $JsonString
                        } catch {
                            Write-Host "Error updating parameters: $($_.Exception.Message)"
                            continue
                        }
    
                        # Reenable refresh schedule for the dataset
                        $enableRefreshBody = @"
    {
    "value": {"enabled": true}
    }
    "@
    
                        try {
                            Invoke-PowerBIRestMethod -Url "https://api.powerbi.com/v1.0/myorg/groups/$($workspace.Id)/datasets/$($Report.DatasetId)/refreshSchedule" -Method Patch -Body ("$enableRefreshBody")
                            Write-Host "Refresh schedule Enabled for dataset: $($Report.DatasetId)"
                        } catch {
                            Write-Host "Failed to Enable refresh schedule: $($_.Exception.Message)"
                        }
    
                        Remove-Variable UpdateParameterList, JsonString -ErrorAction SilentlyContinue
                    } else {
                        Write-Host "Version already matches. Skipping update." # Log if no update is needed
                    }
                } else {
                    Write-Host "No parameters found for this dataset." # Log if no parameters are found
                }
            }
        } else {
            Write-Host "No reports found in workspace: $($workspace.Name)" # Log if no reports are found in the workspace
        }
    }
    
    # Log the completion of the script
    Write-Host "Script completed."

    This approach has saved me hours on PBIX deployment.
    If you discover any useful additions or tweaks, feel free to reach out on LinkedIn or by email I’d love to hear how you’ve adapted it.

    Thanks for reading!

    https://github.com/AetherAdv/powerbi_powershell_updatereports