SQL Data Grid

An example of a data grid that queries and updates data in SQL.

This example uses Invoke-DbaQuery from dbatools to query and update data in a SQL database. It implements paging, sorting and filtering via the data grid's -LoadRows event handler. It also renders buttons within the table that updates the database based on selections.

Additionally, bulk actions are performed using checkbox selection.

This example requires the following table.

CREATE TABLE [dbo].[Certificates](
	[CertificateId] [int] NULL,
	[Decision] [varchar](255) NULL,
	[UserName] [varchar](255) NULL,
	[GroupName] [varchar](255) NULL,
	[AppName] [varchar](255) NULL
) ON [PRIMARY]

The data grid is defined within a dynamic region with some basic properties.

New-UDDynamic -Id "DynamicTable" {
      New-UDDataGrid -ShowPagination -PageSize 10 -RowsPerPageOptions @(10, 25, 50, 100, 200) -CheckboxSelection -LoadRows {

The -LoadRows event handler does much of the work but custom columns also provide an avenue for updating the database. We'll break down this example below.

Columns

The columns used in this example are basic, aside from a decision column that is used to update the SQL database.

The basic columns to display look like this. Properties that are returned from the SQL query will be available in render methods but will not be displayed.

New-UDDataGridColumn -HeaderName 'User' -Field 'UserName' -Filterable -Sortable -Description 'UserName'
New-UDDataGridColumn -HeaderName 'Group' -Field 'GroupName'  -Filterable -Sortable
New-UDDataGridColumn -HeaderName 'App' -Field 'AppName' -Filterable -Sortable

We will implement a complex column to update the SQL database. The $EventData variable will include the entire row's data. We can use this data to render buttons based on its state. If the certificate is approved, we will create green buttons. If the certificate is revoked, we will create red buttons. Clicking the buttons will run an UPDATE command and then reload the dynamic region.

New-UDDataGridColumn -HeaderName 'Decision' -Field 'Decision' -MinWidth 300 -Render {     
    New-UDDynamic -Id $EventData.CertificateId -Content {
        function New-DecisionButton {
            param(
                $Text,
                $Icon,
                $Style,
                $CertificateId,
                $Decision
            )

            New-UDButton -Text $Text -Icon $Icon -Style $Style -OnClick {
                $Session:updateButton = $true
                $Query = @"
            UPDATE Certificates
            SET Decision = $Decision
            WHERE CertificateId = $CertificateId;
"@

                Invoke-DbaQuery -SqlInstance '(localdb)\MSSQLLocalDB' -Database PSU -Query $Query
                Sync-UDElement -Id $CertificateId
            } -ShowLoading
        }

        if ($Session:updateButton) {
            $item = Invoke-DbaQuery -SqlInstance '(localdb)\MSSQLLocalDB' -Database PSU -Query ('SELECT * FROM [dbo].[Certificates] WHERE CertificateId = {0}' -f $EventData.CertificateId)
            $EventData.Decision = $item.Decision
            $Session:updateButton = $false
        }

        if ($EventData.Decision -eq 1) {
            New-DecisionButton -Text 'Approve' -Icon $Page:IconApprove -Style $Page:BtnGreenApprove -CertificateId $EventData.CertificateId -Decision 'NULL'
            New-DecisionButton -Text 'Revoke' -Icon $Page:IconRevokeNeutral -Style $Page:BtnWhiteRevoke -CertificateId $EventData.CertificateId -Decision '0'
        }
        elseif ($EventData.Decision -eq 0) {
            New-DecisionButton -Text 'Approve' -Icon $Page:IconApproveNeutral -Style $Page:BtnWhiteApprove -CertificateId $EventData.CertificateId -Decision '1'
            New-DecisionButton -Text 'Revoke' -Icon $Page:IconRevoke -Style $Page:BtnRedRevoke -CertificateId $EventData.CertificateId -Decision 'NULL'
        }
        else {
            New-DecisionButton -Text 'Approve' -Icon $Page:IconApproveNeutral -Style $Page:BtnWhiteApprove -CertificateId $EventData.CertificateId -Decision '1'
            New-DecisionButton -Text 'Revoke' -Icon $Page:IconRevokeNeutral -Style $Page:BtnWhiteRevoke -CertificateId $EventData.CertificateId -Decision '0'
        }
    }
}

Filtering

Filtering is performed by accessing the $EventData variable (assigned to $Context in this example). Filters are available as a property of the context. Filters also include the logic operator used. This will be a and or or operator. We construct a SQL where expression and populate a hashtable of parameters.

$SqlFilter = ""
$Parameters = @{}
if ($Context.Filter.Items.Count -gt 0) {
    $count = 1
    $ParameterName = "P$count"
    foreach ($filter in $Context.Filter.Items) {
        if ($count -gt 1) {
            $SqlFilter += " $($Context.Filter.logicOperator) "
        }
        else {
            $SqlFilter += " WHERE "
        }

        $Value = $Filter.Value

        switch ($filter.Operator) {
            "contains" { $SqlFilter += " $(ConvertTo-ProperCase $filter.Field) LIKE CONCAT('%', @$ParameterName, '%') " }
            "equals" { $SqlFilter += " $(ConvertTo-ProperCase $filter.Field) = @$ParameterName " }
            "startsWith" { $SqlFilter += " $(ConvertTo-ProperCase $filter.Field) LIKE CONCAT(@$ParameterName, '%') " }
            "endsWith" { $SqlFilter += " $(ConvertTo-ProperCase $filter.Field) LIKE CONCAT('%', @$ParameterName) " }
            "isAnyOf" {
                $Value = $Value -join ','
                $SqlFilter += " $(ConvertTo-ProperCase $filter.Field) IN (SPLIT_STRING(@$ParameterName)) "
            }
            "isempty" { $SqlFilter += " TRIM ($(ConvertTo-ProperCase $filter.Field)) IS NULL " }
            "isnotempty" { $SqlFilter += " TRIM ($(ConvertTo-ProperCase $filter.Field)) IS NOT NULL " }
            "notequals" { $SqlFilter += " $(ConvertTo-ProperCase $filter.Field) != @$ParameterName " }
            "notcontains" { $SqlFilter += " $(ConvertTo-ProperCase $filter.Field) NOT LIKE CONCAT('%', @$ParameterName,'%') " }
        }

        $Parameters[$ParameterName] = $Value
        $count += 1
    }
}

The ConvertTo-ProperCase function adjusts the field name, which will be lower case, into the correct case used for the SQL server columns.

Paging

Paging data is also sent via the context. We can calculate the offset and page size using the following code.

$PageSize = $Context.PageSize
$Offset = $Context.Page * $PageSize
$sqlPage = "OFFSET $Offset ROWS FETCH NEXT $($Context.PageSize) ROWS ONLY;"

Sorting

Columns used for sorting and their direction are also included in the context. This example uses single column sorting but the sort value is an array if multi-column sort is enabled.

if ($Context.Sort[0].field -ne $null) {
    $sqlSort = "ORDER BY $(ConvertTo-ProperCase $Context.Sort[0].field) $($Context.Sort[0].sort) "
}
else {
    $sqlSort = "ORDER BY (SELECT NULL)"
}

Execution

Once we have constructed the SQL query portions and the parameters from the context, we can invoke our query. First, we need to get a total count of rows.

$Query = '{0} {1}' -f $Query, "$SqlFilter $sqlSort $sqlPage"
$Count = Invoke-DbaQuery -SqlInstance '(localdb)\MSSQLLocalDB' -Database PSU -As SingleValue -Query "SELECT COUNT(*) as count FROM dbo.Certificates $SqlFilter" -SqlParameter $Parameters

Once we have our count, the next step is to run the query. We return the rows as PSObjects. We need to format the return data to include the rows and the count.

$Data = Invoke-DbaQuery -SqlInstance '(localdb)\MSSQLLocalDB' -Database PSU -As PSObjectArray -Query $Query -SqlParameter $Parameters

@{
    rows     = [Array]$Data
    rowCount = $Count
}

Bulk Actions

Using the selection feature of the data grid, we can record the row selections and then perform bulk actions using a select dropdown.

The first step is to store the selections in session state. The $EventData variable of OnSelectionChanged is a list of the IDs of the rows. The data grid requires an ID to be defined for each row. If no ID property is found on a row, a random one is generated. Since we want deterministic selections, we use the CertificateID property as an ID in the select to allow for us to use that in queries. Below we store the selected IDs in a session variable and update a label for the select dropdown.

-OnSelectionChange {
    $Session:Selections = $EventData
    if ($Session:Selections.Count -le 0) {
        $Page:TickedCount = ''    
    }
    else {
        $Page:TickedCount = ' ({0})' -f $Session:Selections.Count
    }                
    Sync-UDElement -Id 'DropDownDynamic'
} 

The select drop down uses the selections and then issues an UPDATE command for each of this. This could be optimized to execute a single command with a list of IDs. Once the code the OnChange is run, the table is updated and the drop down is refreshed.

New-UDDynamic -Id "DropDownDynamic" {
    New-UDSelect -Id 'DropDown' -DefaultValue 'Bulk Decisions' -Option {
        New-UDSelectOption -Name "Bulk Decisions$Page:TickedCount" -Value "Bulk Decisions"
        New-UDSelectOption -Name 'Approve' -Value 'Approve'
        New-UDSelectOption -Name 'Revoke' -Value 'Revoke'
    } -OnChange {
        $Choice = $EventData
        $DecisionBit = if ($Choice -eq 'Approve') { 1 } else { 0 }
        $Selected = $Session:Selections
        # Show-UDToast ('{0}' -f $Session:Selections | Out-String) -Persistent
        foreach ($Row in $Selected) {
            $Query = @"
                UPDATE Certificates
                SET Decision = $DecisionBit
                WHERE CertificateId = $Row;
"@
            Invoke-DbaQuery -SqlInstance '(localdb)\MSSQLLocalDB' -Database PSU -Query $Query
        }
        Start-Sleep -Seconds 1
        $Page:TickedCount = ''
        $Session:Selection = @()
        Sync-UDElement -Id 'DropDownDynamic'
        Sync-UDElement -Id 'DynamicTable'
    }
}

Demo

Below is a demo of this app.

Complete Example

The entire implementation of the app can be found below.

New-UDApp -Content {

    $Session:Selections = @()
    #endregion ID Verification and Database Connection
    #region Button and Chip Values and Styles
    $Session:SaveBtnClickedFromOpen = $false
    $Session:SaveBtnClickedFromReview = $false

    $Session:ChipLabelUnderOpen = ''
    $Session:ChipLabelUnderReview = ''

    # White
    $Page:BtnWhiteApprove = @{'background-color' = '#FFFFFF'; 'color' = '#717171'; Width = "90px"; Height = "28px"; 'margin-left' = '0px'; 'margin-right' = '2px' }
    $Page:BtnWhiteRevoke = @{'background-color' = '#FFFFFF'; 'color' = '#717171'; Width = "90px"; Height = "28px"; 'margin-left' = '2px'; 'margin-right' = '0px' }

    # Green
    $Page:BtnGreenApprove = @{'background-color' = '#00842b'; 'color' = '#FFFFFF'; Width = "90px"; Height = "28px"; 'margin-left' = '0px'; 'margin-right' = '2px' }

    # Red
    $Page:BtnRedRevoke = @{'background-color' = '#EB0A1E'; 'color' = '#FFFFFF'; Width = "90px"; Height = "28px"; 'margin-left' = '2px'; 'margin-right' = '0px' }

    # Modal
    $Page:BtnWhiteModal = @{'background-color' = '#FFFFFF'; 'color' = '#717171'; Width = "10px"; Height = "28px"; 'margin-left' = '5px'; 'margin-right' = '0px'; 'min-width' = '10px' }

    $Page:IconModal = New-UDIcon -Icon 'bars' -Color '#717171' -Title 'Approve' -Solid

    $Page:IconApproveNeutral = New-UDIcon -Icon 'thumbs-up' -Color '#717171' -Title 'Approve' -Solid
    $Page:IconApprove = New-UDIcon -Icon 'thumbs-up' -Color '#FFFFFF' -Title 'Approve' -Solid

    $Page:IconRevokeNeutral = New-UDIcon -Icon 'thumbs-down' -Color '#717171' -Title 'Revoke' -Solid
    $Page:IconRevoke = New-UDIcon -Icon 'thumbs-down' -Color '#FFFFFF' -Title 'Revoke' -Solid

    $Page:TextApprove = 'Approve'
    $Page:TextRevoke = 'Revoke'

    #endregion Button and Chip Values and Styles
    #region CSS Styles
    New-UDHtml -Markup '<style>
.my-custom-tabs .MuiTab-root {
    width: 100px;  /* Fixed width */
    text-align: center; /* Center text */
    height: 48px;  /* Fixed height */
    margin-top: -10px;  /* Move text upwards on the page */
}
.my-custom-tabs .MuiTab-root.Mui-selected {
    background-color: #FFFFFF;
    color: #1f2328;
    font-weight: bold;
}
.my-custom-tabs .css-7mcwe1-MuiTabs-indicator {
    background-color: #EB0A1E;
}
.css-nmu7bk-MuiTableRow-root,
.css-1gjuw6o-MuiTableRow-root {
    vertical-align: baseline !important;
}
.my-badge-container {
    position: relative;
    display: flex;
    align-items: center;
}
.my-chip-open {
    position: absolute;
    top: 119px;
    left: 75px;
    z-index: 1000;
    color: #FFFFFF;
    background-color: #666666;
    width: 52px;  /* Fixed width */
    height: 15px;
    text-align: center;  /* Center the text horizontally */
    font-size: 10px;     /* Adjusted font size */
    font-weight: bold;   /* Set font weight to bold */
}
.my-chip-review {
    position: absolute;
    top: 119px;
    left: 174px;
    z-index: 1000;
    color: #FFFFFF;
    background-color: #666666;
    width: 52px;  /* Fixed width */
    height: 15px;
    text-align: center;  /* Center the text horizontally */
    font-size: 10px;     /* Adjusted font size */
    font-weight: bold;   /* Set font weight to bold */
}
</style>'



    #endregion CSS Styles
    # Adding line breaks to push the content down
    New-UDHtml -Markup '<br><br>'
    #region Grid Container (1) Wraps all code
    New-UDGrid -Container -Content {
        New-UDPaper -Elevation 1 -Content {
            #endregion Grid Container (1) Wraps all code
            #region Grid Item (1.1) Tabs
            New-UDGrid -Item -ExtraSmallSize 12 -Content {
                #endregion Grid Item (1.1) Tabs
                New-UDHtml -Markup '<div class="my-badge-container">'  # Open container div
                #region Grid Container (2) Dropdown and Button
                New-UDGrid -Container -Content {
                    New-UDPaper -Elevation 1  -Content {
                        #endregion Grid Item (2) Dropdown and Button
                        #region Grid Item (2.1) Dropdown and Button
                        New-UDGrid -Item -ExtraSmallSize 3 -Content {
                            #endregion Grid Item (2.1) Dropdown and Button
                            #region Dropdown Select
                            New-UDDynamic -Id "DropDownDynamic" {
                                New-UDSelect -Id 'DropDown' -DefaultValue 'Bulk Decisions' -Option {
                                    New-UDSelectOption -Name "Bulk Decisions$Page:TickedCount" -Value "Bulk Decisions"
                                    New-UDSelectOption -Name 'Approve' -Value 'Approve'
                                    New-UDSelectOption -Name 'Revoke' -Value 'Revoke'
                                } -OnChange {
                                    $Choice = $EventData
                                    $DecisionBit = if ($Choice -eq 'Approve') { 1 } else { 0 }
                                    $Selected = $Session:Selections
                                    # Show-UDToast ('{0}' -f $Session:Selections | Out-String) -Persistent
                                    foreach ($Row in $Selected) {
                                        $Query = @"
                                            UPDATE Certificates
                                            SET Decision = $DecisionBit
                                            WHERE CertificateId = $Row;
"@
                                        Invoke-DbaQuery -SqlInstance '(localdb)\MSSQLLocalDB' -Database PSU -Query $Query
                                    }
                                    Start-Sleep -Seconds 1
                                    $Page:TickedCount = ''
                                    $Session:Selection = @()
                                    Sync-UDElement -Id 'DropDownDynamic'
                                    Sync-UDElement -Id 'DynamicTable'
                                }
                            }
                        }
                        #endregion Dropdown Select
                    }
                }
                #region Grid Container (3) Table
                New-UDGrid -Container -Content {
                    #endregion Grid Container (3) Table
                    #region Grid Item (3.1) Table
                    New-UDGrid -Item -ExtraSmallSize 12 -Content {
                        New-UDPaper -Elevation 1  -Content {
                            #endregion Grid Item (3.1) Table
                            #region Table + Filter and Sorts
                            New-UDDynamic -Id "DynamicTable" {
                                New-UDDataGrid -ShowPagination -PageSize 10 -RowsPerPageOptions @(10, 25, 50, 100, 200) -CheckboxSelection -LoadRows {

                                    function ConvertTo-ProperCase {
                                        param($Field)

                                        switch ($Field) {
                                            "username" { "UserName" }
                                            "groupname" { "GroupName" }
                                        }
                                    }

                                    $Context = $EventData

                                    $SqlFilter = ""
                                    if ($Context.Filter.Items.Count -gt 0) {
                                        $count = 1
                                        foreach ($filter in $Context.Filter.Items) {
                                            if ($count -gt 1) {
                                                $SqlFilter += " $($Context.Filter.logicOperator) "
                                            }
                                            else {
                                                $SqlFilter += " WHERE "
                                            }
                                            switch ($filter.Operator) {
                                                "contains" { $SqlFilter += " $(ConvertTo-ProperCase $filter.Field) LIKE '%$($filter.Value)%' " }
                                                "equals" { $SqlFilter += " $(ConvertTo-ProperCase $filter.Field) = '$($filter.Value)' " }
                                                "startsWith" { $SqlFilter += " $(ConvertTo-ProperCase $filter.Field) LIKE '$($filter.Value)%' " }
                                                "endsWith" { $SqlFilter += " $(ConvertTo-ProperCase $filter.Field) LIKE '%$($filter.Value)' " }
                                                "isAnyOf" {
                                                    $count = 1
                                                    foreach ($val in $filter.Value) {
                                                        if ($count -gt 1) {
                                                            $list += ", '$val'"
                                                        }
                                                        else {
                                                            $list += "'$val'"
                                                        }  
                                                        $count += 1
                                                    }
                                                    $SqlFilter += " $(ConvertTo-ProperCase $filter.Field) IN ($($list)) "
                                                }
                                                "isempty" { $SqlFilter += " TRIM ($(ConvertTo-ProperCase $filter.Field)) IS NULL " }
                                                "isnotempty" { $SqlFilter += " TRIM ($(ConvertTo-ProperCase $filter.Field)) IS NOT NULL " }
                                                "notequals" { $SqlFilter += " $(ConvertTo-ProperCase $filter.Field) != '$($filter.Value)' " }
                                                "notcontains" { $SqlFilter += " $(ConvertTo-ProperCase $filter.Field) NOT LIKE '%$($filter.Value)%' " }
                                            }
                                            $count += 1
                                        }
                                    }

                                    $PageSize = $Context.PageSize
                                    # Calculate the number of rows to skip
                                    $Offset = $Context.Page * $PageSize

                                    #endregion Table + Filter and Sorts
                                    #region Create Table Query Herestring
                                    $Query = @"
                                    SELECT
                                        CertificateId,
                                        CertificateId as id,
                                        Decision,
                                        GroupName,
                                        UserName,
                                        AppName
                                    FROM 
                                        dbo.Certificates
"@
                                    #endregion Create Table Query Herestring                                 
                                    #region Table Construct Database Query with `Where`, `Paging`, and `Count`

                                    if ($Context.Sort[0].field -ne $null) {
                                        $sqlSort = "ORDER BY $(ConvertTo-ProperCase $Context.Sort[0].field) $($Context.Sort[0].sort) "
                                    }
                                    else {
                                        $sqlSort = "ORDER BY (SELECT NULL)"
                                    }

                                    $sqlPage = "OFFSET $Offset ROWS FETCH NEXT $($Context.PageSize) ROWS ONLY;"

                                    $Query = '{0} {1}' -f $Query, "$SqlFilter $sqlSort $sqlPage"
                                    $Count = Invoke-DbaQuery -SqlInstance '(localdb)\MSSQLLocalDB' -Database PSU -As SingleValue -Query "SELECT COUNT(*) as count FROM dbo.Certificates $SqlFilter"

                                    Show-UDToast $Query -Persistent
                                    #endregion Table Construct Database Query with `Where's` and `Paging`
                                    #region Query the Database and Pass `$Session:Data` to Out-UDTableData
                                    $Session:Data = Invoke-DbaQuery -SqlInstance '(localdb)\MSSQLLocalDB' -Database PSU -As PSObjectArray -Query $Query

                                    @{
                                        rows     = [Array]$Session:Data
                                        rowCount = $Count
                                    }
                                    #endregion Query the Datbase and Pass `$Session:Data` to Out-UDTableData
                                    #region Table Columns
                                } -Columns @(
                                    New-UDDataGridColumn -HeaderName 'User' -Field 'UserName' -Filterable -Sortable -Description 'UserName'
                                    New-UDDataGridColumn -HeaderName 'Group' -Field 'GroupName'  -Filterable -Sortable
                                    New-UDDataGridColumn -HeaderName 'App' -Field 'AppName' -Filterable -Sortable
                                    New-UDDataGridColumn -HeaderName 'Decision' -Field 'Decision' -MinWidth 300 -Render { 
                                        New-UDDynamic -Id $EventData.CertificateId -Content {

                                            function New-DecisionButton {
                                                param(
                                                    $Text,
                                                    $Icon,
                                                    $Style,
                                                    $CertificateId,
                                                    $Decision
                                                )

                                                New-UDButton -Text $Text -Icon $Icon -Style $Style -OnClick {
                                                    $Session:updateButton = $true
                                                    $Query = @"
                                                UPDATE Certificates
                                                SET Decision = $Decision
                                                WHERE CertificateId = $CertificateId;
"@

                                                    Invoke-DbaQuery -SqlInstance '(localdb)\MSSQLLocalDB' -Database PSU -Query $Query
                                                    Sync-UDElement -Id $CertificateId
                                                } -ShowLoading
                                            }

                                            if ($Session:updateButton) {
                                                $item = Invoke-DbaQuery -SqlInstance '(localdb)\MSSQLLocalDB' -Database PSU -Query ('SELECT * FROM [dbo].[Certificates] WHERE CertificateId = {0}' -f $EventData.CertificateId)
                                                $EventData.Decision = $item.Decision
                                                $Session:updateButton = $false
                                            }

                                            if ($EventData.Decision -eq 1) {
                                                New-DecisionButton -Text 'Approve' -Icon $Page:IconApprove -Style $Page:BtnGreenApprove -CertificateId $EventData.CertificateId -Decision 'NULL'
                                                New-DecisionButton -Text 'Revoke' -Icon $Page:IconRevokeNeutral -Style $Page:BtnWhiteRevoke -CertificateId $EventData.CertificateId -Decision '0'
                                            }
                                            elseif ($EventData.Decision -eq 0) {
                                                New-DecisionButton -Text 'Approve' -Icon $Page:IconApproveNeutral -Style $Page:BtnWhiteApprove -CertificateId $EventData.CertificateId -Decision '1'
                                                New-DecisionButton -Text 'Revoke' -Icon $Page:IconRevoke -Style $Page:BtnRedRevoke -CertificateId $EventData.CertificateId -Decision 'NULL'
                                            }
                                            else {
                                                New-DecisionButton -Text 'Approve' -Icon $Page:IconApproveNeutral -Style $Page:BtnWhiteApprove -CertificateId $EventData.CertificateId -Decision '1'
                                                New-DecisionButton -Text 'Revoke' -Icon $Page:IconRevokeNeutral -Style $Page:BtnWhiteRevoke -CertificateId $EventData.CertificateId -Decision '0'
                                            }
                                        }
                                    }
                                    #region Table OnRowSelection
                                ) -OnSelectionChange {
                                    $Session:Selections = $EventData
                                    if ($Session:Selections.Count -le 0) {
                                        $Page:TickedCount = ''    
                                    }
                                    else {
                                        $Page:TickedCount = ' ({0})' -f $Session:Selections.Count
                                    }                
                                    Sync-UDElement -Id 'DropDownDynamic'
                                } 
                            }
                        }
                    }
                }
                #endregion Table OnRowSelection
            }
        }
    }

}

Last updated

Copyright 2022 Ironman Software