Power BI component framework
One area in which Power BI decidedly lacks is in support for UI design.
Let's say that we have a slicer object that is the same across all pages in the entire report. Power BI allows you to sync the filtering state across pages, but it doesn't allow you to "sync" the physical state of the slicer. What if we reposition the slicer on one of the pages? What if we make one of them longer? What if we change the colour of the slicer header font? All of these changes need to be manually handled and kept track of, to make sure that all of the changes are propagated properly.
One workaround is to make a change in one of the slicers, delete all of the old ones on all of the other pages, and copy the new, changed slicers, and paste it in all of the other places it belongs. But then bookmarks would be broken - this "new" slicer object will need to be re-fitted to existing bookmarks.
Here is one of my efforts to create more powerful tools for Power BI.
This PowerShell script allows for the "syncing" of all objects of the same type and title.
Here is how it works:
- Have a page with a name that starts with "Primitives"
- You can have several pages called "Primitives 1", "Primitives 2", "Primitives Slicers", "Primitives Shapes", and so on
- Create a new object. Set it exactly the way you want, and give it a title
- Don't forget to give it a title
- Copy the object across all of the pages that it is supposed to live in
- Don't change the title
That's it for setup.
Before you run the script, make sure that your shell in inside the folder where the .pbip file lives - this is its reference point. When you run the script, it will pick up on all of the objects in the pages called "Primitives". It will give you a choice on which objects to set. It will then update all objects that match the selected object primitives. It does this by overwriting the properties of the old objects with the properties of the primitive. If you have a shape that "sets" a bookmark, this script will retain that bookmark reference so you won't lose it.
I have some other ideas on how to take this further. The first thing is renaming all like objects; the second is deleting all like objects.
There are some limitations:
- I don't think it's possible to create new objects programmatically. Power BI assigns a unique ID to every object in the report, and I don't want to attempt creating such an ID.
- This technique is tied to the type and title of the object. As such, renaming the title is not one of the changes that are support here.
# get all the objects on page name primitives
$path = (pwd).path
$project = Split-Path $path -Leaf
$reportFile = "$path\$project.Report\report.json"
$report = cat $reportFile | ConvertFrom-Json
$sections = $report.sections
$sectionPrimitives = $sections | ? { $_.displayName.StartsWith("Primitives")}
if ($sectionPrimitives.Count -eq 0) {
Write-Host "There is no page called `"Primitives`". You'll need this page to define primitive components"
break;
}
$visualContainersPrimitives = $sectionPrimitives.visualContainers |
? {
$null -eq ($_.config | ConvertFrom-Json).singleVisualGroup
}
$primitives = $visualContainersPrimitives |
% {
$config = $_.config | ConvertFrom-Json
[PSCustomObject] @{
name = "$($config.singleVisual.visualType) $($config.singleVisual.vcObjects.title.properties.text.expr.Literal.Value) $($config.name)";
definition = $_
}
}
# # for every object, find all the corresponding objects in all other pages
$primitives | ocgv |
% {
$_ = $_.definition
$configPrimitive = $_.config | ConvertFrom-Json
$type = $configPrimitive.singleVisual.visualType
$name = $configPrimitive.singleVisual.vcObjects.title.properties.text.expr.Literal.Value
Write-Host "Updating component $type $name"
$hasBookmark = $null -ne $configPrimitive.singleVisual.vcObjects.visualLink.properties.bookmark.expr.Literal.Value
# if ($null -ne $bookmark) {
# Write-Host $bookmark
# $No = New-Object System.Management.Automation.Host.ChoiceDescription '&No', 'No'
# $Yes = New-Object System.Management.Automation.Host.ChoiceDescription '&Yes', 'Yes'
# $Options = [System.Management.Automation.Host.ChoiceDescription[]]($No, $Yes)
# $proceed = $host.ui.PromptForChoice('This object has a bookmark on it. This operation will overwrite the bookmarks on all matching objects. Proceed?', $Message, $Options, 0)
# if ($proceed -eq 0) {
# Write-Host Skipping...`n
# return;
# }
# }
[PSCustomObject] $sections |
? {
-not $_.displayName.StartsWith('Primitives')
} |
% {
$sectionDisplayName = $_.displayName
[PSCustomObject] $visualContainers = $_.visualContainers
$visualContainers |
% {
$config = $_.config | ConvertFrom-Json
$_type = $config.singleVisual.visualType
$_name = $config.singleVisual.vcObjects.title.properties.text.expr.Literal.Value
$_id = $config.name
if (($_type -eq $type) -and ($_name -eq $name)) {
Write-Host "✅ $sectionDisplayName ($_id)"
$bookmark = $config.singleVisual.vcObjects.visualLink.properties.bookmark.expr.Literal.Value
$config.layouts = $configPrimitive.layouts
$config.singleVisual = $configPrimitive.singleVisual
if ($hasBookmark) {
$config.singleVisual.vcObjects.visualLink.properties.bookmark.expr.Literal.Value = $bookmark
}
$_.config = $config | ConvertTo-Json -Depth 20 -Compress
} else { $_ }
$_
}
$_.visualContainers = $visualContainers
$_
} | out-null
Write-Host Finished with component $type $name`n
}
[PSCustomObject] $report | convertto-json -Depth 20 > $reportFile