MAIN MENU
Devolutions Blog

Announcements, updates, and insights from Devolutions.

PowerShell
Thumbnail for Separating UI and data in PSU: Building apps and endpoints the right way

Separating UI and data in PSU: Building apps and endpoints the right way

This second PSU series article shows how to separate UI from data by combining Apps and Endpoints in PowerShell Universal. Learn how to build reusable dashboards, expose data through web services, and create cleaner, more scalable automation workflows.

In this second part of the series, we’ll separate the UI from the data. This allows you to build elegant and powerful UI dashboards with just a few lines of code, while leveraging web services to provide the necessary data.

As you’ll see at the end, this approach offers enormous flexibility and lets you tackle challenges with ease—challenges that would typically cause a lot of headaches.

Quick recap: PSU apps

If you followed part 1, you’ll immediately appreciate the first significant advantage that PSU provides: it acts as a management system and repository for your code, allowing you to easily reuse previous work.

For example, the “service list” we created in part 1 is still available in PSU. Whenever you need more information about your services, you can access it via the main PSU dashboard:

Remember that every PSU app is directly accessible via its unique URL, so you can open it by navigating to its link, for example: http://localhost:5000/list-services/Home

Building an action dashboard

We’ll build a central dashboard with navigation buttons. Like anything with a UI, dashboards are just PSU Apps, so we’ll start where we left off last time:

# use a scriptblock to save the different pages to one variable

$Pages = & {

  # FIRST PAGE (root page) defines your cockpit with all of your buttons

  New-UDPage -Name 'Home' -Url '/' -Content {

    # add a small header:

    New-UDTypography -Text 'My personal Dashboard' -Variant h6

    # add three buttons

    New-UDButton -Text 'Services' -OnClick {

      # tell the button what to do when clicked

      # use -Native to open an external app (URL is relative to PSU root)

      Invoke-UDRedirect '/list-services' -Native

    }

    New-UDButton -Text 'Processes' -OnClick {

      # tell the button what to do when clicked

      # DO NOT use -Native to open a child page within this app (URL is relative to this app)

      Invoke-UDRedirect '/list-processes'

    }

    # third button, same as before

    New-UDButton -Text 'Something else' -OnClick {

      Invoke-UDRedirect '/list-more'

    }

  }



  # add as many CHILD PAGES as you need. Their URL is relative to the root URL of this app

  New-UDPage -Name 'Processes' -Url '/list-processes' -Content {

    New-UDTypography -Text 'Here you could implement your own code to display something more useful.'

  }



  New-UDPage -Name 'More' -Url '/list-more' -Content {

    New-UDTypography -Text 'Here you could implement your own code to display something more useful.'

  }

}

New-UDApp -Title 'My Cockpit' -Pages $Pages

This example illustrates a few core concepts, so let’s first explore the interface and then look at the details:

Calling external apps

The “Services” button demonstrates how you can embed external apps in your dashboard. By using Invoke-UDRedirect '/list-services' with the -Native switch, you can specify URLs relative to the main PSU root and access any other app you created earlier.

While it is possible to incorporate external apps this way, it is not considered good practice because it comes at a cost: you lose automatic navigation. The hamburger menu in the top-left corner is no longer available in the external app. This is expected, since you are effectively navigating to a completely different app.

Calling child pages

The other two buttons work differently: they navigate to child pages of the dashboard app. They use the same UDRedirect cmdlet but without the -Native switch, so the URL refers to an internal child page created within your dashboard using New-UDPage.

Because these child pages are part of your dashboard app, automatic navigation works as expected, and the hamburger menu remains visible regardless of which page is currently displayed.

Since you define these child pages within your dashboard app, you have full control and can easily add UI features in a consistent way—for example, by adding a more prominent “Back to Home” button.

Here is an example. Simply replace your code (and don’t forget to click the Save button before viewing the app):

function New-CockpitBackBar {

    New-UDStack -Direction row -Spacing 2 -Children {

        New-UDButton -Text '← Home' -OnClick {

            Invoke-UDRedirect '/'

        }

    }

}

# use a scriptblock to save the different pages to one variable

$Pages = & {

  # FIRST PAGE (root page) defines your cockpit with all of your buttons

  New-UDPage -Name 'Home' -Url '/' -Content {

    # add a small header

    New-UDTypography -Text 'My personal Dashboard' -Variant h6

    # add three buttons

    New-UDButton -Text 'Services' -OnClick {

      # tell the button what to do when clicked

      # use -Native to open an external app (URL is relative to PSU root)

      Invoke-UDRedirect '/list-services' -Native

    }

    New-UDButton -Text 'Processes' -OnClick {

      # tell the button what to do when clicked

      # DO NOT use -Native to open a child page within this app (URL is relative to this app)

      Invoke-UDRedirect '/list-processes'

    }

    # third button, same as before

    New-UDButton -Text 'Something else' -OnClick {

      Invoke-UDRedirect '/list-more'

    }

  }



  # add as many CHILD PAGES as you need. Their URL is relative to the root URL of this app

  New-UDPage -Name 'Processes' -Url '/list-processes' -Content {

    New-CockpitBackBar

    New-UDTypography -Text 'Here you could implement your own code to display something more useful.'

  }



  New-UDPage -Name 'More' -Url '/list-more' -Content {

    New-CockpitBackBar

    New-UDTypography -Text 'Here you could implement your own code to display something more useful.'

  }

}

New-UDApp -Title 'My Cockpit' -Pages $Pages

Now, on the child pages, you’ll find a “Home” button that conveniently takes you back to the dashboard root.


Home button

What about code reuse?

Using child pages comes with trade-offs: you cannot reuse the work you invested in previous apps. For child pages to display useful content, you need to add the appropriate code yourself.

This leads us to better design strategies. In part 1, we invested effort in creating comprehensive service lists and implemented them as a single app. The app worked as expected, but as you can see now, it is not reusable. A better approach would have been to separate data and UI into distinct components—and that’s exactly what we’re going to do today.

In part 1, we introduced PSU apps as the UI (presentation) layer. To complete the picture, we will now introduce PSU endpoints as the data layer.

Creating an endpoint (aka Web service)

To make the service list information we developed in part 1 truly reusable, we should have implemented it as a web service instead of an app. Let’s fix that now:

function Get-ServiceProcessId {

    [CmdletBinding()]

    param

    (

        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ServiceName')]

        [string]

        $Name,



        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ActualService')]

        [System.ServiceProcess.ServiceController]

        $Service,



        [switch]

        $PassThru

    )

    begin

    {

        $hash = Get-CimInstance Win32_Service -ErrorAction Ignore |

        Group-Object -Property Name -AsHashTable -AsString

    }



    process

    {

        # since the user now has the option to either specify the service name

        # or pipe in an actual service, let's look up the pendant so we always

        # have both and can simplify the remaining code

        if ($PSCmdlet.ParameterSetName -eq 'ServiceName')

        {

            $Service = Get-Service -Name $Name -ErrorAction Ignore

        }

        else

        {

            $Name = $Service.Name

        }

        # make sure "0" values are replaced by NULL

        $id = $hash.$Name.ProcessId | Where-Object { $_ -gt 0 }



        if ($PassThru)

        {

            # take the service and add the process ID

            $Service |

            Add-Member -MemberType NoteProperty -Name ProcessId -Value $id -PassThru

        }

        else

        {

            $id

        }

    }

}

# pipe in any object with a property "ProcessId" or "Id", and add a property with

# the actual process name

function Get-ProcessNameById {

    [CmdletBinding()]

    param

    (

        [Parameter(Mandatory, ValueFromPipeline)]

        [Object]

        $InputObject,



        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]

        [int]

        [Alias('Id')]

        $ProcessId,



        [switch]

        $PassThru

    )



    process

    {

        $ProcessName = if ($ProcessId)

        {

            (Get-Process -Id $ProcessId).Name

        }

        if ($PassThru)

        {

            $InputObject |

            Add-Member -MemberType NoteProperty -Name ProcessName -Value $ProcessName -PassThru

        }

        else

        {

            $ProcessName

        }

    }

}

# submit the name of a service, or pipe in services

# adds the "GroupName" property, exposing the name of the service group for

# the given service

function Get-ServiceGroupName

{

    [CmdletBinding()]

    param

    (

        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ServiceName')]

        [string]

        $Name,



        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ActualService')]

        [System.ServiceProcess.ServiceController]

        $Service,



        [switch]

        $PassThru

    )

    begin

    {

        $hash = Get-CimInstance Win32_Service |

        Group-Object -Property Name -AsHashTable -AsString

    }



    process

    {

        # since the user now has the option to either specify the service name

        # or pipe in an actual service, let's look up the pendant so we always

        # have both and can simplify the remaining code

        if ($PSCmdlet.ParameterSetName -eq 'ServiceName')

        {

            $Service = Get-Service -Name $Name -ErrorAction Ignore

        }

        else

        {

            $Name = $Service.Name

        }

        # get the launch command for this service

        $pathName = $hash.$Name.PathName

        # look if the "-k" parameter was specified, followed by the service group name

        # we are after (uses RegEx)

        $groupName = if($pathName -match '-k\s+(\w+)')

        {

            $matches[1]

        }



        if ($PassThru)

        {

            # take the service and add the process ID

            $Service |

            Add-Member -MemberType NoteProperty -Name GroupName -Value $groupName -PassThru

        }

        else

        {

            $groupName

        }

    }

}

Get-Service -ErrorAction Ignore |

    # ...add the service process ID...

    Get-ServiceProcessId -PassThru |

    # ...take only services that have a process ID...

    Where-Object ProcessId |

    # ...add the process name for the process ID...

    Get-ProcessNameById -PassThru |

    # ...add the service group name so we can differentiate svchost...

    Get-ServiceGroupName -PassThru |

    # ...sort by service displayname...

    Sort-Object -Property DisplayName |

    # select the properties to display in the table

    Select-Object -Property DisplayName, Name, ProcessName, GroupName, ProcessId

By default, the web service you created is secured and requires authentication. We’ll cover authentication and security options separately, so for now we will disable authentication:

Your web service is now accessible to anyone, including external PowerShell sessions. Let’s test it by opening a regular PowerShell console. Your web service has a unique URL—in our example: http://localhost:5000/get-service. Simply launch any PowerShell console and run:

PS> Invoke-RestMethod -Uri http://localhost:5000/get-service

The web service returns the data to you—mission accomplished: data and UI are now separated.


Web service PowerShell

Final dashboard version

Now that you know how to clearly separate data from UI, we can revisit our dashboard app and focus entirely on the UI. We no longer rely on external apps; instead, we keep everything UI-focused.

function New-CockpitBackBar {

    New-UDStack -Direction row -Spacing 2 -Children {

        New-UDButton -Text '← Home' -OnClick {

            Invoke-UDRedirect '/'

        }

    }

}

$Pages = & {

  New-UDPage -Name 'Home' -Url '/' -Content {

    New-UDTypography -Text 'My personal Dashboard' -Variant h6

    New-UDButton -Text 'Services' -OnClick {

      # do NOT go to a separate App, handle everything WITHIN this App (do NOT use -Native)

      Invoke-UDRedirect '/list-services'

    }

    New-UDButton -Text 'Processes' -OnClick {

      Invoke-UDRedirect '/list-processes'

    }

    New-UDButton -Text 'Something else' -OnClick {

      Invoke-UDRedirect '/list-more'

    }

  }



  # handle service list in a CHILD PAGE

  New-UDPage -Name 'Services' -Url '/list-services' -Content {

    New-CockpitBackBar



    # get the data from a web service (reusable, modular, flexible)

    $data = Invoke-RestMethod -Uri http://localhost:5000/get-service

    New-UDTable -Data $data

  }

  New-UDPage -Name 'Processes' -Url '/list-processes' -Content {

    New-CockpitBackBar

    New-UDTypography -Text 'Here you could implement your own code to display something more useful.'

  }



  New-UDPage -Name 'More' -Url '/list-more' -Content {

    New-CockpitBackBar

    New-UDTypography -Text 'Here you could implement your own code to display something more useful.'

  }

}

New-UDApp -Title 'My Cockpit' -Pages $Pages

Click “View App” to test-drive this final version. You will see the same three buttons, but this time, when you click “Services,” you seamlessly navigate to a child page displaying the enhanced service list. At that point, the hamburger menu is still available, you have access to the “Home” button, and all the complexity involved in creating the service list is now offloaded to a web service.


Services

Next part

Now that we’ve clarified how to cleanly separate data from UI, you’ve seen two of the main features in PSU: “Apps,” which serve as the presentation layer, and “Endpoints,” which are essentially web services that provide raw data.

In the upcoming parts, we’ll dive deeper into the advantages PSU can offer with this approach:

More from PowerShell

Read more articles