Broadcast update dynamic sections

Product: PowerShell Universal
Version: 2.9.3

Heyas,

This may not even be possible, but I’m looking for a way to sync UD elements for all people who currently have a dashboard open - in a push method. Similar to the Show-UDToast broadcast messages.

I have a dashboard page that displays SQL Job info, allows you to start the SQL job, and displays the current SQL job info…

idle:
Imgur

running:
Imgur

This is fine for the user who starts the job, because the start button calls a function that uses dbatools to start the sql job then loops and when noticing a change in the sql job info (jumps to the next step perhaps) it then updates a cache: variable (using Set-Item -PSPath "cache:$($JobName)SQLJobInfo" -Value $jobinfo) followed by a sync Sync-UDElement -Id "$JobName``JobInfo which updates the job info on the dashboard for the person who clicked the start button.

What I’m after is an experience where if 2 people have the same dashboard page open, and one of them starts the job, the job info section will update for both people. The id of the job info’s dynamic section is the same for both people, and the job info data is set to the same named cache: variable but only one person’s dynamic section is updating when the Sync-UDElement -Id "$JobName``JobInfo command is issued from the external function.

Should I be using Set-PSUCache -Key $($JobName)SQLJobInfo -Value $jobinfo / Get-PSUCache instead of Set-Item -PSPath "cache:$($JobName)SQLJobInfo" -Value $jobinfo maybe?

Cheers!
Steve.

Is this something that is impossible?

Is the method to achieve this to “poll” from the dashboard? It just seem expensive calling dbaTool’s Get-SqlAgentJob every cpl of seconds to see if the state of the job has changed.

You can use Sync-UDElement -Id 'name' -Broadcast to send it to all users of the dashboard. You can use Sync-PSUComponent from within a job or API to sync all connected users to all dashboards.

In this circumstance, I might recommend Sync-PSUComponent because even jobs run from the admin console would cause dashboards to reload.

1 Like

So easy

Thanks!

Hey man, if you don’t mind me asking:

I love the way the job info looks, especially how you show the progress dynamically.
I’m also creating a similar thing but for the creation of new users. Would you mind sharing some of your code? Mainly interested in how you “live update” the progress on the card.

Would be greatly appreciated!

Heya Tom,

Sure thing, I’ll dump the relevant code below. I use this for displaying the current state of SQL Agent Jobs (and being able to start them form the dashboard). This was put together a while ago originally and I’ve learned a lot about PSU since then so there’s probably loads of stuff I’m doing wrong or not the best way - so if anyone has any suggestions please let me know! Always worth refactoring :slight_smile:

I’ve been using the SQLServer and dbaTools modules so you’ll need them. I’ve found getting SQL job info is much faster using the SQLServer module and I’m changing to that (from dbaTools).

The main PSU dashboard .ps1 file initialises a bunch of variables that are assigned as cache variables based on a .json file like…

$BaseConfigFile = "config-uat.json"
$BaseConfigFolder = (Join-Path $PSScriptRoot "websitename-config")
# Load and parse the JSON configuration file
try {
	$Config = Get-Content (Join-Path -Path $BaseConfigFolder -ChildPath $BaseConfigFile) -Raw -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue | ConvertFrom-Json -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue
} catch {
	Throw -Message "The websitename-config Base configuration file is missing!"
}
# Check the configuration
if (!($Config)) {
	Throw "The websitename-config Base configuration file is corrupt! Check for dodgy syntax somewhere."
}

...
...
...
$cache:cfgSQLserver = ($Config.SQL.Settings.SQLserver)
$cache:logoDarkBlue = ($Config.Colors.LogoDarkBlue)
$cache:logoGreen = ($Config.Colours.LogoGreen)
...
...
...

There are 2 relevant functions (Check-SQLJob and Start-SQLJob) that I load (along with many others) using the main PSU dashboard .ps1 file, it dot sources any functions within .ps1 files that are in the \websitename-functions\ folder…

### DOT SOURCE REQUIRED FUNCTIONS/SCRIPTS ###
Get-ChildItem -Path (Join-Path $PSScriptRoot "websitename-functions") -Filter *.ps1 -Recurse -File | Where-Object { $_.FullName -ne $PSCommandPath } | ForEach-Object {
    . $_.FullName
}

Check-SQLJob function (gets the job info of the SQL job and assigns values to cache variables then broadcasts an element sync out to the dashboards which updates them) - I’ll probably rename this function to something like Sync-SQLJob:

function Check-SQLJob {
    param (
        [Parameter()]
        [String]$JobName
    )

    Show-UDToast -Message "Refreshing SQL Job info for: $JobName" -Duration 3000
    
    $JobNameWithSpaces = $JobName -replace '_',' '

    #Get-SqlAgentJob is MUCH faster than dbaTools Get-DbaAgentJob PLUS it includes LastRunDuration whereas dbaTools doesn't
    #$jobinfo = (Get-DbaAgentJob -SqlInstance $cache:cfgSQLserver -Job "$JobNameWithSpaces" |
    $jobinfo = (Get-SqlAgentJob -ServerInstance $cache:cfgSQLserver -Name "$JobNameWithSpaces" |
        Select-Object @{Name = 'CurrentRunStatus'; Expression = {$c = $_.CurrentRunStatus; "$c"}},@{Name = 'CurrentRunStep'; Expression = {$step = $_.CurrentRunStep; if($step -eq '0 (unknown)'){"Not running"}else{"$step"}}},Description,IsEnabled,LastRunDate,@{Name = 'LastRunOutcome'; Expression = {$l = $_.LastRunOutcome; "$l"}},NextRunDate,@{Name = 'State'; Expression = {$s = $_.State; "$s"}},LastRunDuration,JobSteps)
    Set-Item -PSPath "cache:$($JobName)SQLJobInfo" -Value $jobinfo
        
    if((Get-Item -pspath "cache:$($JobName)SQLJobInfo").CurrentRunStatus -eq "Idle") {
        # this is to set jobfinished to true so that the popup "complete" appears (when cache:$($JobName)JobFinished is true)
        # silently continue on error because the first time this runs for a job, the varialbe "cache:$($JobName)JobRunning" won't exist so throws an error that is ok to ignore
        If((Get-Item -pspath "cache:$($JobName)JobRunning" -ErrorAction SilentlyContinue) -eq $true) { 
            # job was running, now finished, also sync dynamic sections above jobinfo sections
            Set-Item -PSPath "cache:$($JobName)JobFinished" -Value $true
        }

        Set-Item -PSPath "cache:$($JobName)JobRunning" -Value $false
        Set-Item -PSPath "cache:$($JobName)ButtonPointerEvents" -Value 'auto'
        Set-Item -PSPath "cache:$($JobName)ButtonColour" -Value $cache:logoGreen
        Set-Item -PSPath "cache:$($JobName)ButtonText" -Value ("run sql job ""$JobNameWithSpaces"" on $cache:cfgSQLserver")

    } else {

        Set-Item -PSPath "cache:$($JobName)JobFinished" -Value $false
        Set-Item -PSPath "cache:$($JobName)JobRunning" -Value $true
        Set-Item -PSPath "cache:$($JobName)ButtonPointerEvents" -Value 'none'
        Set-Item -PSPath "cache:$($JobName)ButtonColour" -Value 'LightGrey'
        Set-Item -PSPath "cache:$($JobName)ButtonText" -Value "sql job currently running..."
    }

    Sync-UDElement -Id "$JobName`JobInfo" -Broadcast
    Sync-UDElement -Id "btnstartjob$JobName" -Broadcast

} # end function

Start-SQLJob function (starts the SQL job (if it hasn’t been started already) then loops checking every 3 seconds to see if the SQL job info has changed, like moving on a step, if there is a change then it calls the Check-SQLJob function):

function Start-SQLJob {
    
    [CmdletBinding()]
    param (
        [Parameter()]
        [String]$JobName
    )
    $JobNameWithSpaces = $JobName -replace '_',' '

    # need to update this to use Get-SqlAgentJob as it's MUCH faster than Get-DbaAgentJob
    if((Get-DbaAgentJob -SqlInstance $cache:cfgSQLserver -Job "$JobNameWithSpaces").CurrentRunStatus -eq "Idle"){
        Start-DbaAgentJob -SqlInstance $cache:cfgSQLserver -Job "$JobNameWithSpaces"
    }
    
    $sqlJobTimeoutSeconds = 0
    $previousMonitoringSqlJobInfo = $null
    Do {
        $ts = [timespan]::fromseconds($sqlJobTimeoutSeconds)

        Show-UDToast -Message "sqlJob ($JobName) is running: $(("{0:hh\:mm\:ss}" -f $ts))" -duration 3000
        
        # need to update this to use Get-SqlAgentJob as it's MUCH faster than Get-DbaAgentJob, as well as removing a bunch of unrequired properties, only need CurrentRunStatus and CurrentRunStep
        Set-Item -PSPath "cache:$($JobName)monitoringSqlJobInfo" -Value (Get-DbaAgentJob -SqlInstance $cache:cfgSQLserver -Job "$JobNameWithSpaces" |
            Select-Object @{Name = 'CurrentRunStatus'; Expression = {$c = $_.CurrentRunStatus; "$c"}},@{Name = 'CurrentRunStep'; Expression = {$step = $_.CurrentRunStep; if($step -eq '0 (unknown)'){"Not running"}else{"$step"}}},Description,IsEnabled,LastRunDate,@{Name = 'LastRunOutcome'; Expression = {$l = $_.LastRunOutcome; "$l"}},NextRunDate,@{Name = 'State'; Expression = {$s = $_.State; "$s"}},LastRunDuration,JobSteps)
       
        #only check/sync the job element if anything has changed in the sql job info
        if( ($null -eq $previousMonitoringSqlJobInfo) -Or ($previousMonitoringSqlJobInfo.CurrentRunStatus -ne ((Get-Item -pspath "cache:$($JobName)monitoringSqlJobInfo")).CurrentRunStatus) -Or ($previousMonitoringSqlJobInfo.CurrentRunStep -ne ((Get-Item -pspath "cache:$($JobName)monitoringSqlJobInfo")).CurrentRunStep) ) {
            Check-SQLJob -JobName $JobName
        }

        $previousMonitoringSqlJobInfo = Get-Item -pspath "cache:$($JobName)monitoringSqlJobInfo"
        Start-Sleep -Seconds 3
        $sqlJobTimeoutSeconds += 3
    } Until ( ((Get-Item -pspath "cache:$($JobName)monitoringSqlJobInfo").CurrentRunStatus -eq "Idle") -Or $sqlJobTimeoutSeconds -gt 1200)
    
    #dumb timeout to alert user sql job running longer than 20minutes
    if($sqlJobTimeoutSeconds -gt 1200){
        Show-Popup -type "error" -Message "SQL Job $JobName timed out (ran longer than 20 minutes)" -Duration 10000 -CloseOnClick $true -Broadcast $true -IncludeUserNameAndTime $false -PlayAlertSound $false
    }
    
} # end function

Show-Popup function (just a wrapper around Show-UDToast):

function Show-Popup {
    param (
        $type = "info",
        $closeonclick = $false,
        $broadcast = $false,
        $message,
        $duration = 8000,
        $includeusernameandtime = $false,
        $playalertsound = $false
    )
    $toastParamsCommon = @{
        Message = If($includeusernameandtime) {"[$(Get-Date -format "HH:mm:ss")] $(If($null -eq $session:UDuser.displayname){$User}else{$session:UDuser.displayname}): $message"}else{$message}
        Duration = $duration
        CloseOnClick = $closeonclick
        Broadcast = $broadcast
        Position = 'topCenter'
        TransitionIn = 'fadeInDown'
    }
    switch ($type) {
        "error" {
            $toastParamsSpecific = @{
                BackgroundColor = "pink"
                MessageColor = "red"
            }
        }
        "info" {
            $toastParamsSpecific = @{
                BackgroundColor = "lightblue"
                MessageColor = "blue"
            }
        }
        "success" {
            $toastParamsSpecific = @{
                BackgroundColor = "lightgreen"
                MessageColor = "green"
            }
        }
    }
    Show-UDToast @toastParamsCommon @toastParamsSpecific
    If($playalertsound) {
        Invoke-UDJavascript -Javascript "var audio = new Audio('$(("$cache:cfgSiteRootURL/assets/fanfare-trumpet.wav").Replace('//','/'))');audio.play();"
    }

} # end function

For the dashboard page, I use the URL parameter to bring in the SQL Job Name, like /JobProcessing?JobName=Run_this_SQL_job so I have $JobName in the page as the name of the SQL Job to start.

Here’s the dashboard code with a lot removed but hopefully enough for the job running part…

...
...
...
If (-not($JobName)) {
    Exit
}
$JobNameWithSpaces = $JobName -replace '_',' '

Check-SQLJob -JobName $JobName
...
...
...
New-UDPaper -Content {

	New-UDGrid -Container -Content {

        Set-Item -PSPath "cache:$($JobName)JobFinished" -Value $false
		New-UDDynamic -Id "$JobName`JobInfo" -Content {
				
			if((Get-Item -pspath "cache:$($JobName)JobFinished") -eq $true) {
				Show-Popup -type "success" -Message "$JobName Job Complete!" -Duration 30000 -CloseOnClick $true -Broadcast $true -IncludeUserNameAndTime $false -PlayAlertSound $false
				Set-Item -PSPath "cache:$($JobName)JobFinished" -Value $false
			}
				
			New-UDGrid -Container -Content {
				New-UDGrid -Item -ExtraSmallSize 12 -SmallSize 12 -MediumSize 12 -LargeSize 12 -Content {
					New-UDTypography -Text $JobName -Variant 'h6'
					New-UDTypography -Text ((Get-Item -pspath "cache:$($JobName)SQLJobInfo").Description) -Variant 'body2'
					if((Get-Item -pspath "cache:$($JobName)JobRunning") -eq $true) {
						New-UDTypography -Text "[running step $( (Get-Item -pspath "cache:$($JobName)SQLJobInfo").CurrentRunStep ) of $(((Get-Item -pspath "cache:$($JobName)SQLJobInfo").JobSteps).Count)]" -Variant 'overline'
					} else {
						New-UDTypography -Text "[not running - last result: $((Get-Item -pspath "cache:$($JobName)SQLJobInfo").LastRunOutcome)]" -Variant 'overline'
					}
				}
			}

				
			New-UDGrid -Container -Content {
				
				New-UDGrid -Item -ExtraSmallSize 12 -SmallSize 12 -MediumSize 12 -LargeSize 12 -Content {
					New-UDList -Content {
						New-UDListItem -Label 'CurrentRunStatus' -Icon (New-UDIcon -Icon cogs -Size lg -Color $cache:logoDarkBlue) -SubTitle (Get-Item -pspath "cache:$($JobName)SQLJobInfo").CurrentRunStatus
						$maxsteps=((Get-Item -pspath "cache:$($JobName)SQLJobInfo").JobSteps).Count
						for($i=1; $i -le $maxsteps; $i++) {
							New-UDStyle -Style "		
								$(if((Get-Item -pspath "cache:$($JobName)SQLJobInfo").CurrentRunStep -ne 'Not running'){
									"font-style: italic !important;
									color: LightGrey !important;"
									if( ((Get-Item -pspath "cache:$($JobName)SQLJobInfo").CurrentRunStep).SubString(0,1) -eq $i ) {
										"font-style: underline !important;
										font-style: normal !important;
										color: Black !important;"
									} elseif ( ((Get-Item -pspath "cache:$($JobName)SQLJobInfo").CurrentRunStep).SubString(0,1) -gt $i ) {
										"text-decoration: line-through;"
										"font-weight: normal;"
									}
								} else {
									"color: LightGrey !important;"
								}
								)
							" -Content {
								New-UDListItem -Label "$i`. $((Get-Item -pspath "cache:$($JobName)SQLJobInfo").JobSteps | where {$_.Id -eq $i})" -Icon (New-UDIcon -Icon caret_right -Size lg -Color $cache:logoDarkBlue)
							}
						}
					}
				}
			}    
			
			New-UDGrid -Container -Content {
				New-UDGrid -Item -ExtraSmallSize 12 -SmallSize 6 -MediumSize 5 -LargeSize 3 -Content {
					New-UDList -Content {
						New-UDListItem -Label 'CurrentRunStep' -Icon (New-UDIcon -Icon info -Size lg -Color $cache:logoDarkBlue) -SubTitle (Get-Item -pspath "cache:$($JobName)SQLJobInfo").CurrentRunStep
					}
				}
			
				New-UDGrid -Item -ExtraSmallSize 12 -SmallSize 6 -MediumSize 5 -LargeSize 3 -Content {
					New-UDList -Content {
						New-UDListItem -Label 'LastRunDate' -Icon (New-UDIcon -Icon info -Size lg -Color $cache:logoDarkBlue) -SubTitle (Get-Date((Get-Item -pspath "cache:$($JobName)SQLJobInfo").LastRunDate) -Format "dd MMM yyyy HH:mm:ss")
					}
				}                       

				New-UDGrid -Item -ExtraSmallSize 12 -SmallSize 6 -MediumSize 5 -LargeSize 3 -Content {
					New-UDList -Content {
						New-UDListItem -Label 'LastRunOutcome' -Icon (New-UDIcon -Icon info -Size lg -Color $cache:logoDarkBlue) -SubTitle (Get-Item -pspath "cache:$($JobName)SQLJobInfo").LastRunOutcome
						
					}
				}
			
				New-UDGrid -Item -ExtraSmallSize 12 -SmallSize 6 -MediumSize 5 -LargeSize 3 -Content {
					New-UDList -Content {
						New-UDListItem -Label 'LastRunDuration' -Icon (New-UDIcon -Icon info -Size lg -Color $cache:logoDarkBlue) -SubTitle $([string](Get-Item -pspath "cache:$($JobName)SQLJobInfo").LastRunDuration)
					}
				}
			}

			If((Get-Item -pspath "cache:$($JobName)JobRunning") -eq $true) {
				New-UDStyle -Style "
				.MuiCircularProgress-colorPrimary {
					color: $cache:logoGreen
				}" -Content {
					New-UDProgress -Circular 
				}                                                           
			}

			New-UDButton -Id "btnbegin$JobName" -Text (Get-Item -pspath "cache:$($JobName)ButtonText") -OnClick {
				If((Get-Item -pspath "cache:$($JobName)JobRunning") -eq $false) {
					Show-UDModal -Content {
						New-UDTypography -Text "Are you sure you want to start this process?"
					} -Footer {
						New-UDButton -Text "Close" -Variant 'outlined' -OnClick { 
							Hide-UDModal
						}
						New-UDButton -Text "Start" -OnClick { 
							Hide-UDModal
							
							Show-Popup -type "info" -Message "Started $JobNameWithSpaces sql job!" -Duration 30000 -CloseOnClick $true -Broadcast $true -IncludeUserNameAndTime $true -PlayAlertSound $false
							
							Set-Item -PSPath "cache:$($JobName)ButtonPointerEvents" -Value 'none'
							Set-Item -PSPath "cache:$($JobName)ButtonColour" -Value 'LightGrey'
							Set-Item -PSPath "cache:$($JobName)ButtonText" -Value "sql job currently running..."
							Set-Item -PSPath "cache:$($JobName)JobRunning" -Value $true
							
							Sync-UDElement -Id "$JobName`JobInfo" -Broadcast
							Sync-UDElement -Id "btnstartjob$JobName" -Broadcast
							# start job as separate PS function
							Start-SQLJob -JobName $JobName

						}
					} -Persistent
					
				} else {
					Show-UDToast -Message "job was already running! checking for updated job info" -Duration 10000
					Check-SQLJob -JobName $JobName
				}
				
			} -Style @{
				'background-color' = Get-Item -PSPath "cache:$($JobName)ButtonColour"
				'color' = 'White'
				'margin' = 'auto'
				'pointer-events' = Get-Item -PSPath "cache:$($JobName)ButtonPointerEvents"
			}

		} #end dynamic job info
		
	}

} -Elevation 4

New-UDElement -Tag 'div' -Attributes @{
	style = @{
		"position"="relative"
		"float" = "right"
		"margin-left"="auto"
		"margin-top"="-45px"
		"margin-right"="-15px"
	}
} -Content {
	New-UDFloatingActionButton -Icon (New-UDIcon -Icon undo -Size lg) -OnClick {
		Check-SQLJob -JobName $JobName
	}
}
...
...
...                      

Hopefully there’s enough there for you to work out what’s going on. Anyway, I might’ve missed some parts out, see how ya go.

Cheers!
Steve

1 Like

Hey man,

This is very much appreciated. Looks very promising for my situation! I’ll give it a try next week. Thank you!