Created
April 30, 2024 09:44
-
-
Save JPRuskin/90e9c086b673e743b2a33df760af7571 to your computer and use it in GitHub Desktop.
PowerShell Universal App to display the last X Chocolatey Community Package build results, highlighting failures and showing failures that have an existing PR in place.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#requires -Version 6.1 | |
[CmdletBinding()] | |
param( | |
[uint16]$Last = 20 | |
) | |
if (-not $cache:CachedCalls) { | |
$cache:CachedCalls = @{} | |
} | |
function Get-GistFileRawUrl { | |
[OutputType([string[]])] | |
[CmdletBinding()] | |
param( | |
# Full url to the gist | |
[Parameter(Mandatory, ParameterSetName="Url")] | |
[string]$GistUrl = "https://gist.github.com/$($Owner)/$($GistID)", | |
# The owner account of the gist | |
[Parameter(Mandatory, ParameterSetName="Split")] | |
[string]$Owner, | |
# The ID of the gist | |
[Parameter(Mandatory, ParameterSetName="Split")] | |
[string]$GistID, | |
# Filename within the gist | |
[Parameter(Mandatory)] | |
[string]$Name, | |
# The number of revisions to return | |
[uint]$Revisions = 1 | |
) | |
end { | |
if ($PSCmdlet.ParameterSetName -eq 'Url' -and $GistUrl -match "^https://(?:gist.githubusercontent.com|gist.github.com)/(?<Owner>.+)/(?<GistId>.+)/?") { | |
$Owner, $GistID = $Matches.Owner, $Matches.GistID | |
} | |
if ($Revisions -gt 1) { | |
$GistRequest = @{ | |
Uri = "https://api.github.com/gists/$($GistID)/commits" | |
Headers = @{ | |
Accept = "application/vnd.github+json" | |
} | |
} | |
if ($env:github_api_token) { | |
$GistRequest.Headers += @{Authorization = "Bearer $($env:github_api_token)"} | |
} elseif ($secret:GitHubPAT) { | |
$GistRequest.Headers += @{Authorization = "Bearer $($secret:GitHubPAT)"} | |
} | |
$Requested = 0 | |
$PerPage = [math]::Min($Revisions, 100) | |
$Page = 1 | |
$(while ($Requested -lt $Revisions) { | |
Invoke-RestMethod @GistRequest -Body @{ | |
per_page = $PerPage | |
page = $Page++ | |
} -Headers @{ | |
} | |
$Requested += $PerPage | |
}).ForEach{$_}.ForEach{ | |
"https://gist.githubusercontent.com/$($Owner)/$($GistId)/raw/$($_.version)/$($Name)" | |
} | Select-Object -First $Revisions | |
} else { | |
"https://gist.githubusercontent.com/$($Owner)/$($GistId)/raw/$($Name)" | |
} | |
} | |
} | |
function Get-GitHubRepositoryPullRequestNumber { | |
[CmdletBinding()] | |
[OutputType([uint16[]])] | |
param( | |
[Parameter()] | |
[string]$Repository = "chocolatey-community/chocolatey-packages", | |
[hashtable]$Filter = @{ | |
state = 'open' | |
base = 'master' | |
sort = 'updated' | |
direction = 'desc' | |
} | |
) | |
end { | |
$GitHubRequest = @{ | |
Uri = "https://api.github.com/repos/$($Repository)/pulls" | |
Headers = @{ | |
Accept = "application/vnd.github+json" | |
"X-GitHub-Api-Version" = "2022-11-28" | |
} | |
} | |
if ($env:github_api_token) { | |
$GitHubRequest.Headers += @{Authorization = "Bearer $($env:github_api_token)"} | |
} elseif ($secret:GitHubPAT) { | |
$GistRequest.Headers += @{Authorization = "Bearer $($secret:GitHubPAT)"} | |
} | |
if (-not $cache:CachedCalls) {$cache:CachedCalls = @{}} | |
if (-not $cache:CachedCalls[$GitHubRequest.Uri]) { | |
$cache:CachedCalls[$GitHubRequest.Uri] = Invoke-RestMethod @GitHubRequest -Body $Filter | |
} | |
$cache:CachedCalls[$GitHubRequest.Uri].Number | |
} | |
} | |
function Get-ChocolateyPackageUpdatesInGitHubPR { | |
<# | |
.Synopsis | |
#> | |
[CmdletBinding()] | |
param( | |
[Parameter()] | |
[string]$Repository = "chocolatey-community/chocolatey-packages", | |
[Parameter(Mandatory, ValueFromPipeline)] | |
[uint16[]]$PR = $(Get-GitHubRepositoryPullRequestNumber) | |
) | |
begin { | |
$GitHubRequest = @{ | |
Headers = @{ | |
Accept = "application/vnd.github+json" | |
"X-GitHub-Api-Version" = "2022-11-28" | |
} | |
} | |
if ($env:github_api_token) { | |
$GitHubRequest.Headers += @{Authorization = "Bearer $($env:github_api_token)"} | |
} elseif ($secret:GitHubPAT) { | |
$GistRequest.Headers += @{Authorization = "Bearer $($secret:GitHubPAT)"} | |
} | |
if (-not $cache:CachedCalls) {$cache:CachedCalls = @{}} | |
} | |
process { | |
foreach ($PR in $PR) { | |
$GitHubRequest.Uri = "https://api.github.com/repos/$($Repository)/pulls/$($PR)/files" | |
if (-not $cache:CachedCalls[$GitHubRequest.Uri]) { | |
$cache:CachedCalls[$GitHubRequest.Uri] = Invoke-RestMethod @GitHubRequest -UseBasicParsing | |
} | |
$PackageFolderRegex = "^(?<PackageType>automatic|deprecated|extensions|manual)\/(?<PackageId>.+?)\/.+" # Arguably we could just match on automatic for AU | |
[PSCustomObject]@{ | |
PR = $PR[0] | |
PackageId = @($cache:CachedCalls[$GitHubRequest.Uri].FileName) -match $PackageFolderRegex -replace $PackageFolderRegex,'${PackageId}' | Select-Object -Unique | |
} | |
} | |
} | |
} | |
function ParseMarkdownTable { | |
param( | |
[Parameter(Mandatory, ParameterSetName = "Markdig")] | |
$Table, | |
[Parameter(Mandatory, ParameterSetName = "String")] | |
[string[]]$TableString = $Table.Inline.Content[0].Text.Split("`n"), | |
[Parameter(Mandatory)] | |
[ValidateSet("Pushed","Error","Ignored","OK")] | |
[string]$Type | |
) | |
begin { | |
$script:Regex = @{ | |
"Pushed" = @{ | |
Icon = '<img src="(?<IconUri>.+)?" width="32" height="32"\/>' | |
Name = '\[(?<Name>.+)\]\((?<PackageUrl>.+)\)' | |
Updated = '\[(?<Updated>True|False)\].+' | |
Pushed = '(?<Pushed>True|False)' | |
RemoteVersion = '\[(?<RemoteVersion>.*)?\]\((?<SoftwareUrl>.+)?\)' | |
NuspecVersion = '\[(?<NuspecVersion>.+)\]\((?<SourceUrl>.+)?\)' | |
} | |
"Error" = @{ | |
Icon = '<img src="(?<IconUri>.+)?" width="32" height="32"\/>' | |
Name = '\[(?<Name>.+)\]\((?<PackageUrl>.+)\)' | |
RemoteVersion = '\[(?<RemoteVersion>.*)?\]\((?<SoftwareUrl>.+)?\)' | |
NuspecVersion = '\[(?<NuspecVersion>.+)\]\((?<SourceUrl>.+)?\)' | |
Error = '\[(?<ErrorMessage>.*)\]\((?:.+)\)' | |
} | |
"Ignored" = @{ | |
Icon = '<img src="(?<IconUri>.+)?" width="32" height="32"\/>' | |
Name = '\[(?<Name>.+)\]\((?<PackageUrl>.+)\)' | |
NuspecVersion = '\[(?<NuspecVersion>.+)\]\((?<SourceUrl>.+)?\)' | |
IgnoreMessage = '(?<IgnoreMessage>.*)' | |
} | |
"OK" = @{ | |
Icon = '<img src="(?<IconUri>.+)?" width="32" height="32"\/>' | |
Name = '\[(?<Name>.+)\]\((?<PackageUrl>.+)\)' | |
Updated = '\[?(?<Updated>True|False).+' | |
Pushed = '(?<Pushed>True|False)' | |
RemoteVersion = '\[(?<RemoteVersion>.*)?\]\((?<SoftwareUrl>.+)?\)' | |
NuspecVersion = '\[(?<NuspecVersion>.+)\]\((?<SourceUrl>.+)?\)' | |
} | |
} | |
} | |
end { | |
$Headers = $TableString[0].Split('|').Where{$_} | |
$TableString[2..$TableString.Count].ForEach{ | |
$TableRegex = @( | |
'^' # Start of line | |
$Headers.ForEach{ | |
$script:Regex.$Type.$_ | |
} | |
) -join "\|" | |
if ($_ -match $TableRegex) { | |
$ht = $Matches | |
$ht.Remove("0") | |
} else { | |
Write-Warning "$($Type): '$($_)' did not match '$($TableRegex)'" | |
} | |
# $Line = $_.Split('|') | |
# $ht = [ordered]@{ | |
# Icon = $Line[$($ErrorHeaders.IndexOf('Icon'))] -replace '<img src="(?<Uri>.+)" width="32" height="32"/>','${Uri}' | |
# Name = $Line[$($ErrorHeaders.IndexOf('Name'))] -replace '\[(?<Name>.+)\]\((?<PackageUrl>.+)\)','${Name}' | |
# RemoteVersion = if ($Line[$($ErrorHeaders.IndexOf('RemoteVersion'))] -match '\[(?<RemoteVersion>.+)\]\((?<SoftwareUrl>.+)\)') {$Matches.RemoteVersion} | |
# NuspecVersion = $Line[$($ErrorHeaders.IndexOf('NuspecVersion'))] -replace '\[(?<NuspecVersion>.+)\]\((?<SourceUrl>.+)\)', '${NuspecVersion}' | |
# PackageUrl = $Line[$($ErrorHeaders.IndexOf('Name'))] -replace '\[(?<Name>.+)\]\((?<PackageUrl>.+)\)', '${PackageUrl}' | |
# SourceUrl = $Line[$($ErrorHeaders.IndexOf('NuspecVersion'))] -replace '\[(?<NuspecVersion>.+)\]\((?<SourceUrl>.+)\)','${SourceUrl}' | |
# } | |
# foreach ($Key in @("Icon","Name","RemoteVersion","NuspecVersion")) { | |
# $ht.$Key = $Line[$($ErrorHeaders.IndexOf($Key))] | |
# } | |
[PSCustomObject]$ht | |
} | |
} | |
} | |
function Get-AUOutputFromMarkdown { | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] | |
[Alias('Uri')] | |
[string]$Path | |
) | |
process { | |
if ($cache:CachedCalls[$Path]) { | |
return $cache:CachedCalls[$Path] | |
} | |
$Raw = if (Test-Path $Path) { | |
Get-Content $Path | |
} else { | |
(Invoke-WebRequest $Path -UseBasicParsing).content | |
} | |
$Markdown = $Raw | ConvertFrom-Markdown | |
$Output = [ordered]@{} | |
# $Stats = $Markdown.Tokens.Where{$_.Level -eq 1}[0] # Never used | |
$Pushed = $Markdown.Tokens.Where{$_.Level -eq 2 -and $_.Inline.Content.ToString() -eq 'Pushed'}[0] | |
$Errors = $Markdown.Tokens.Where{$_.Level -eq 2 -and $_.Inline.Content.ToString() -eq 'Errors'}[0] | |
$Ignored = $Markdown.Tokens.Where{$_.Level -eq 2 -and $_.Inline.Content.ToString() -eq 'Ignored'}[0] | |
$Ok = $Markdown.Tokens.Where{$_.Level -eq 2 -and $_.Inline.Content.ToString() -eq 'OK'}[0] | |
if ($Pushed) { | |
$Output.Pushed = ParseMarkdownTable -TableString ($Markdown.Tokens[$Markdown.Tokens.IndexOf($Pushed) + 1].Inline.Content[0].Text.Split("`n")) -Type Pushed | |
} | |
if ($Errors) { | |
$Output.Errors = ParseMarkdownTable -TableString ($Markdown.Tokens[$Markdown.Tokens.IndexOf($Errors) + 1].Inline.Content[0].Text.Split("`n")) -Type Error | |
# The full message from the error is not contained in the table, so we look it up separately and overwrite the partial | |
foreach ($Package in $Output.Errors) { | |
$Package.ErrorMessage = $Markdown.Tokens[$Markdown.Tokens.IndexOf( | |
$Markdown.Tokens.Where{$_.Level -eq 3 -and $_.Inline.Content.ToString() -eq $Package.Name}[0] | |
) + 1].Lines -join "`n" | |
# We also add any open PRs targeting that package | |
Add-Member -InputObject $Package -Name ExistingPR -MemberType NoteProperty -Value (Get-GitHubRepositoryPullRequestNumber | Get-ChocolateyPackageUpdatesInGitHubPR).Where{$_.PackageId -eq $Package.Name}.PR | |
} | |
} | |
if ($Ignored) { | |
$Output.Ignored = ParseMarkdownTable -TableString ($Markdown.Tokens[$Markdown.Tokens.IndexOf($Ignored) + 1].Inline.Content[0].Text.Split("`n")) -Type Ignored | |
} | |
if ($OK) { | |
$Output.OK = ParseMarkdownTable -TableString ($Markdown.Tokens[$Markdown.Tokens.IndexOf($OK) + 1].Inline.Content[0].Text.Split("`n")) -Type OK | |
} | |
# Sort Build Stats | |
$Output.Build = [PSCustomObject]@{ | |
Time = Get-Date (Select-String -InputObject $Raw -Pattern "(?<=\*\*UTC\*\*: )\d{4}-\d{2}-\d{2} \d{2}:\d{2}").Matches.Value | |
AUVersion = "1.0.0" # There's actually no source for this data, turns out, despite the badge. | |
Pushed = $Output.Pushed.Count | |
Errors = $Output.Errors.Count | |
Ignored = $Output.Ignored.Count | |
Report = $Path | |
} | |
$cache:CachedCalls[$Path] = [PSCustomObject]$Output | |
$cache:CachedCalls[$Path] | |
} | |
} | |
function Get-UpdateFlakiness { | |
[CmdletBinding()] | |
[OutputType([HashTable])] | |
param( | |
[Parameter(Mandatory)] | |
[string]$Id, | |
$DataSet = $R | |
) | |
@{ | |
Failed = $DataSet.Where{$_.Errors.Name -eq $Id}.Count | |
Total = $DataSet.Count | |
Series = $DataSet.Where({$_.Errors.Name -notcontains $Id}, "Until").Count | |
LastNonFailure = $DataSet.Where({$_.Errors.Name -notcontains $Id}, "First", 1).Build.Time | |
} | |
} | |
function Get-PackageModerationStatus { | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory)] | |
[string]$Id, | |
[Parameter()] | |
[string]$Version | |
) | |
$Result = if ($Version) { | |
Invoke-RestMethod "https://community.chocolatey.org/api/v2/Packages()?`$filter=(Id eq '$Id' and Version eq '$Version')&includePrerelease=true" | |
} else { | |
Invoke-RestMethod "https://community.chocolatey.org/api/v2/Packages()?`$filter=(Id eq '$Id')&includePrerelease=true" | |
} | |
$Result[-1].properties.PackageStatus | |
} | |
# if (-not $MyInvocation.InvocationName -eq '.') { | |
# $R = Get-GistFileRawUrl -GistUrl "https://gist.github.com/choco-bot/a14b1e5bfaf70839b338eb1ab7f8226f" -Name "Update-AUPackages.md" -Revisions $Last | Get-AUOutputFromMarkdown | |
# $R.Errors.Where{-not $_.ExistingPR}.Name # List to work on | |
# } | |
New-UDDashboard -Title 'Community Packages Dashboard' -Pages @( | |
New-UDPage -Name 'Home' -Title "Chocolatey Community Package Build History" -Content { | |
$R = Get-GistFileRawUrl -GistUrl "https://gist.github.com/choco-bot/a14b1e5bfaf70839b338eb1ab7f8226f" -Name "Update-AUPackages.md" -Revisions $Last | Get-AUOutputFromMarkdown | |
New-UDGridLayout -Content { | |
New-UDCard -Id "LastRun" -Title "Last Run" -Content { | |
New-UDHtml "`n`n`n" | |
New-UDList -Content { | |
New-UDListItem -Icon (New-UDIcon -Icon Clock -Size 2x) -Label "$($R[0].Build.Time)" | |
New-UDListItem -Icon (New-UDIcon -Icon ThumbsUp -Style @{color='green'} -Size 2x) -Label "$($R[0].Build.Pushed)" -SubTitle Pushed | |
New-UDListItem -Icon (New-UDIcon -Icon Check -Size 2x) -Label "$($R[0].OK.Count)" -SubTitle OK | |
New-UDListItem -Icon (New-UDIcon -Icon Meh -Size 2x) -Label "$($R[0].Build.Ignored)" -SubTitle Ignored | |
New-UDListItem -Icon (New-UDIcon -Icon ThumbsDown -Style @{color='red'} -Size 2x) -Label "$($R[0].Build.Errors)" -SubTitle Failed | |
New-UDListItem -Icon (New-UDIcon -Icon Archive -Size 2x) -Label "$($R[0].Build.Pushed + $R[0].OK.Count + $R[0].Build.Ignored + $R[0].Build.Errors)" -SubTitle Total | |
} | |
} -TitleAlignment center -Raised -Sx @{ | |
"background-color" = "background.default" # to troubleshoot | |
} | |
$Chart = @{ | |
Type = "bar" | |
LabelProperty = "Date" | |
Data = @( | |
$R[-1..-$R.Length].ForEach{ | |
@{ | |
Date = "$(Get-Date $_.Build.Time -Format 'MM/dd')" | |
Pushed = $_.Build.Pushed | |
#OK = $_.OK.Count | |
Ignored = $_.Build.Ignored | |
Errors = $_.Build.Errors | |
} | |
} | |
) | |
Dataset = @( | |
#New-UDChartJSDataset -DataProperty OK -Label OK -BackgroundColor lightgrey | |
New-UDChartJSDataset -DataProperty Errors -Label Errors -BackgroundColor red -BorderWidth 2 -AdditionalOptions @{borderRadius=5} | |
New-UDChartJSDataset -DataProperty Ignored -Label Ignored -BackgroundColor lightgray -BorderWidth 2 -AdditionalOptions @{borderRadius=5} | |
New-UDChartJSDataset -DataProperty Pushed -Label Pushed -BackgroundColor green -BorderWidth 2 -AdditionalOptions @{borderRadius=5} | |
) | |
Options = @{ | |
responsive = $true | |
scales = @{ | |
xAxes = @{ | |
stacked = $true | |
} | |
yAxes = @{ | |
stacked = $true | |
} | |
} | |
} | |
} | |
New-UDChartJS @Chart -Id "BuildChart" | |
New-UDTabs -Id "Tables" -Tabs { | |
New-UDTab -Text "Failed" -Content { | |
New-UDTable -Id "ErrorTable" -Data $R[0].Errors -Columns @( | |
New-UDTableColumn -Property Name -Title "Package" -Render { | |
New-UDImage -Url $EventData.IconUri -Width 25 | |
New-UDTypography -Text $EventData.Name -Style @{ | |
"font-size" = "1.2rem" | |
"margin-left" = "20px" | |
} -Sx @{ | |
color = "text.secondary" | |
} | |
} -Truncate | |
New-UDTableColumn -Property Flakiness -Title "Flakiness" -Render { | |
$Flakiness = Get-UpdateFlakiness -Id $EventData.Name | |
New-UDTooltip -Content { | |
"$($Flakiness.Failed / $Flakiness.Total * 100)%" | |
} -TooltipContent { | |
"$($Flakiness.Failed) / $($Flakiness.Total) Failed" | |
New-UDHtml "`n" | |
"$($Flakiness.Series) in a row." | |
if ($Flakiness.LastNonFailure) { | |
New-UDHtml "`n" | |
" Last non-failure: $($Flakiness.LastNonFailure)" | |
} | |
} -Type info | |
} | |
New-UDTableColumn -Property NuspecVersion -Title "Current Version" -Render { | |
New-UDLink -Text $EventData.NuspecVersion -Url "https://community.chocolatey.org/packages/$($EventData.Name)/$($EventData.NuspecVersion)" -OpenInNewWindow | |
} | |
New-UDTableColumn -Property ExistingPR -Title "PR" -Render { | |
if ($EventData.ExistingPR) { | |
New-UDLink -Url "https://github.com/chocolatey-community/chocolatey-packages/pull/$($EventData.ExistingPR)" -Text "#$($EventData.ExistingPR)" | |
} else { | |
New-UDTypography -Text "None" -Sx @{color="text.secondary"} | |
} | |
} | |
New-UDTableColumn -Property ErrorMessage -Title "Error" | |
) -OnRowExpand {} # Consider OnRowExpand New-UDNivoChart -Calendar | |
} -Dynamic | |
New-UDTab -Text "Ignored" -Content { | |
New-UDTable -Id "IgnoredTable" -Data $R[0].Ignored -Columns @( | |
New-UDTableColumn -Property Name -Title "Package" -Render { | |
New-UDImage -Url $EventData.IconUri -Width 25 | |
New-UDTypography -Text $EventData.Name -Style @{ | |
"font-size" = "1.2rem" | |
"margin-left" = "20px" | |
} -Sx @{ | |
color = "text.secondary" | |
} | |
} -Truncate | |
New-UDTableColumn -Property NuspecVersion -Title "Current Version" -Render { | |
New-UDLink -Text $EventData.NuspecVersion -Url "https://community.chocolatey.org/packages/$($EventData.Name)/$($EventData.NuspecVersion)" -OpenInNewWindow | |
} | |
New-UDTableColumn -Property IgnoreMessage -Title "Reason" | |
) | |
} -Dynamic | |
New-UDTab -Text "OK" -Disabled -Content { | |
New-UDTable -Id "OKTable" -Data $R[0].OK -Columns @( | |
New-UDTableColumn -Property Name -Title "Package" -Render { | |
New-UDImage -Url $EventData.IconUri -Width 25 | |
New-UDTypography -Text $EventData.Name -Style @{ | |
"font-size" = "1.2rem" | |
"margin-left" = "20px" | |
} -Sx @{ | |
color = "text.secondary" | |
} | |
} -Truncate | |
New-UDTableColumn -Property NuspecVersion -Title "Current Version" -Render { | |
New-UDLink -Text $EventData.NuspecVersion -Url "https://community.chocolatey.org/packages/$($EventData.Name)/$($EventData.NuspecVersion)" -OpenInNewWindow | |
} | |
) | |
} -Dynamic | |
New-UDTab -Text "Pushed" -Content { | |
New-UDTable -Id "PushedTable" -Data $R[0].Pushed -Columns @( | |
New-UDTableColumn -Property Name -Title "Package" -Render { | |
New-UDImage -Url $EventData.IconUri -Width 25 | |
New-UDTypography -Text $EventData.Name -Style @{ | |
"font-size" = "1.2rem" | |
"margin-left" = "20px" | |
} -Sx @{ | |
color = "text.secondary" | |
} | |
} -Truncate | |
New-UDTableColumn -Property NuspecVersion -Title "Existing Version" -Render { | |
New-UDLink -Text $EventData.NuspecVersion -Url "https://community.chocolatey.org/packages/$($EventData.Name)/$($EventData.NuspecVersion)" -OpenInNewWindow | |
} | |
New-UDTableColumn -Property RemoteVersion -Title "New Version" -Render { | |
New-UDLink -Text $EventData.RemoteVersion -Url "https://community.chocolatey.org/packages/$($EventData.Name)/$($EventData.RemoteVersion)" -OpenInNewWindow | |
} | |
# New-UDTableColumn -Property Pushed -Title "Status" -Render { | |
# Get-PackageModerationStatus -Id $EventData.Name -Version $EventData.RemoteVersion | |
# } | |
) | |
} -Dynamic | |
} -RenderOnActive | |
} -Layout '{"lg":[{"w":2,"h":14,"x":10,"y":0,"i":"grid-element-42050adf-edd1-42ab-9f52-ead3d1727f5a","moved":false,"static":false},{"w":10,"h":14,"x":0,"y":0,"i":"grid-element-6c6dead5-ad51-41fd-903c-745a1eb01285","moved":false,"static":false},{"w":12,"h":3,"x":0,"y":14,"i":"grid-element-082bf0c9-98b1-4e04-85c8-3ec35b77b6d4","moved":false,"static":false}],"md":[{"w":2,"h":13,"x":8,"y":0,"i":"grid-element-LastRun","moved":false,"static":false},{"w":8,"h":13,"x":0,"y":0,"i":"grid-element-BuildChart","moved":false,"static":false},{"w":10,"h":4,"x":0,"y":13,"i":"grid-element-Tables","moved":false,"static":false}]}' #-Design | |
} | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment