En esta segunda parte de la serie, separaremos la interfaz de usuario de los datos. Esto le permite crear paneles de interfaz elegantes y potentes con solo unas pocas líneas de código, mientras aprovecha los servicios web para proporcionar los datos necesarios.
Como verá al final, este enfoque ofrece una enorme flexibilidad y le permite abordar con facilidad retos que normalmente causarían muchos quebraderos de cabeza.
Resumen rápido: apps de PSU
Si ha seguido la primera parte, apreciará de inmediato la primera ventaja significativa que ofrece PSU: actúa como un sistema de gestión y repositorio para su código, lo que le permite reutilizar fácilmente el trabajo anterior.
Por ejemplo, la «lista de servicios» que creamos en la primera parte sigue disponible en PSU. Siempre que necesite más información sobre sus servicios, puede acceder a ella a través del panel principal de PSU:
- Haga clic en Apps en la barra de menú vertical de la izquierda.
- Verá todas sus apps. Localice el «Service Dashboard» creado en la primera parte.
- Haga clic en el icono del globo terráqueo para abrir la app. Se mostrarán sus servicios junto con los detalles adicionales que implementamos, como el nombre del grupo de servicios y el nombre del proceso subyacente.
Recuerde que cada app de PSU es accesible directamente a través de su URL única, por lo que puede abrirla navegando a su enlace, por ejemplo: http://localhost:5000/list-services/Home
Creación de un panel de acciones
Vamos a crear un panel central con botones de navegación. Como cualquier elemento con interfaz de usuario, los paneles son simplemente Apps de PSU, así que retomaremos el trabajo donde lo dejamos la última vez:
- Haga clic en «Apps» en el menú vertical principal de la izquierda y, a continuación, haga clic en el elemento secundario «Apps». Ahora verá una lista de sus apps existentes.
- Haga clic en el botón azul «Create App» situado en la parte superior para añadir una nueva app. Elija «Dashboard1» como nombre e introduzca «dashboard1» como URL. A continuación, haga clic en OK.
- Haga clic en el icono del lápiz para abrir el editor de código. Sustituya el código de ejemplo por el siguiente código:
# 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
- Haga clic en el icono de Guardar (con el símbolo del disquete). Este paso es crucial: debe guardar manualmente cualquier edición, de lo contrario PSU ignorará sus cambios.
- Haga clic en el botón «View App» para ejecutarlo. Ahora verá la interfaz de su panel, que consta de tres botones y un menú de hamburguesa generado automáticamente en la esquina superior izquierda.
Este ejemplo ilustra algunos conceptos fundamentales, así que primero exploraremos la interfaz y luego veremos los detalles:
- Cuando haga clic en «Services», verá la app de lista de servicios que creamos en la primera parte. Si aparece un mensaje de error, es posible que se haya saltado la primera parte, por lo que la app que muestra los servicios no existe.
- Cuando haga clic en «Processes» o en «Something else», navegará a páginas que forman parte de su app de panel. Estas páginas aún no contienen contenido significativo.
Llamar a apps externas
El botón «Services» muestra cómo puede incrustar apps externas en su panel. Si utiliza Invoke-UDRedirect '/list-services' con el modificador -Native, puede especificar URL relativas a la raíz principal de PSU y acceder a cualquier otra app que haya creado anteriormente.
Aunque es posible incorporar apps externas de esta manera, no se considera una buena práctica porque tiene un coste: se pierde la navegación automática. El menú de hamburguesa de la esquina superior izquierda deja de estar disponible en la app externa. Esto es normal, ya que en realidad está navegando a una app completamente distinta.
Llamar a páginas secundarias
Los otros dos botones funcionan de forma diferente: navegan a páginas secundarias de la app de panel. Utilizan el mismo cmdlet UDRedirect, pero sin el modificador -Native, por lo que la URL hace referencia a una página secundaria interna creada dentro de su panel mediante New-UDPage.
Dado que estas páginas secundarias forman parte de su app de panel, la navegación automática funciona como se espera, y el menú de hamburguesa permanece visible independientemente de la página que se muestre en cada momento.
Dado que estas páginas secundarias se definen dentro de su app de panel, tiene control total y puede añadir fácilmente funciones de interfaz de manera coherente, por ejemplo, agregando un botón «Back to Home» más destacado.
A continuación se muestra un ejemplo. Simplemente sustituya su código (y no olvide hacer clic en el botón Save antes de ver la 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
Ahora, en las páginas secundarias, encontrará un botón «Home» que le lleva cómodamente de vuelta a la raíz del panel.

¿Qué pasa con la reutilización de código?
El uso de páginas secundarias implica ciertas concesiones: no se puede reutilizar el trabajo invertido en apps anteriores. Para que las páginas secundarias muestren contenido útil, es necesario añadir usted mismo el código correspondiente.
Esto nos lleva a mejores estrategias de diseño. En la primera parte, invertimos esfuerzo en crear listas de servicios completas y las implementamos como una única app. La app funcionó como se esperaba, pero, como puede ver ahora, no es reutilizable. Un mejor enfoque habría sido separar los datos y la interfaz de usuario en componentes distintos, y eso es exactamente lo que vamos a hacer hoy.
En la primera parte, presentamos las apps de PSU como la capa de interfaz de usuario (presentación). Para completar el panorama, ahora presentaremos los endpoints de PSU como la capa de datos.
Creación de un endpoint (también conocido como servicio web)
Para que la información de la lista de servicios que desarrollamos en la primera parte sea realmente reutilizable, deberíamos haberla implementado como un servicio web en lugar de una app. Vamos a solucionarlo ahora:
- En el menú vertical del panel de PSU, haga clic en «APIs» y, a continuación, en «Endpoints». Ahora verá todos sus servicios web autodefinidos (inicialmente no hay ninguno). Haga clic en «Create Endpoint».
- En el campo «URL», introduzca
get-servicey, a continuación, haga clic en OK. El servicio web aparecerá en la lista. - Haga clic en el icono del lápiz para abrir el editor de código. Ahora puede separar claramente la interfaz de usuario de los datos. A continuación se muestra el código de datos sin procesar de la primera parte:
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
- Haga clic en el icono Save. A continuación, en la parte superior, haga clic en «Test». Después, haga clic en «Invoke» en el lado derecho para ejecutar su servicio web y comprobar que funciona. Debería ver la información del servicio mostrada en formato JSON.
De forma predeterminada, el servicio web que ha creado está protegido y requiere autenticación. Trataremos las opciones de autenticación y seguridad por separado, así que por ahora deshabilitaremos la autenticación:
- En la lista de endpoints del panel de PSU, haga clic en el icono de engranaje de su servicio web.
- En el cuadro de diálogo, abra la pestaña «Security» y desmarque «Authentication». A continuación, haga clic en OK.
Su servicio web ahora es accesible para cualquiera, incluidas las sesiones externas de PowerShell. Vamos a probarlo abriendo una consola normal de PowerShell. Su servicio web tiene una URL única; en nuestro ejemplo: http://localhost:5000/get-service. Simplemente inicie cualquier consola de PowerShell y ejecute:
PS> Invoke-RestMethod -Uri http://localhost:5000/get-service
El servicio web le devuelve los datos: misión cumplida, los datos y la interfaz de usuario ahora están separados.

Versión final del panel
Ahora que sabe cómo separar claramente los datos de la interfaz de usuario, podemos revisar nuestra app de panel y centrarnos por completo en la interfaz de usuario. Ya no dependemos de apps externas; en su lugar, mantenemos todo centrado en la interfaz de usuario.
- En el menú principal del panel de PSU, haga clic en «Apps» y, a continuación, en el submenú «Apps». A la derecha, verá sus apps de PSU.
- Haga clic en el icono del lápiz de su app «Dashboard1» para abrir el código. Sustitúyalo por el nuevo código y, a continuación, haga clic en el icono Save.
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
Haga clic en «View App» para probar esta versión final. Verá los mismos tres botones, pero esta vez, al hacer clic en «Services», navegará sin problemas a una página secundaria que muestra la lista de servicios mejorada. En ese punto, el menú de hamburguesa sigue disponible, tiene acceso al botón «Home», y toda la complejidad implicada en la creación de la lista de servicios ahora se delega a un servicio web.

Próxima parte
Ahora que hemos aclarado cómo separar claramente los datos de la interfaz de usuario, ha visto dos de las principales funciones de PSU: «Apps», que sirven como capa de presentación, y «Endpoints», que son básicamente servicios web que proporcionan datos sin procesar.
En las próximas partes, profundizaremos en las ventajas que PSU puede ofrecer con este enfoque:
- Por ahora, hemos deshabilitado por completo la autenticación. En las próximas partes, exploraremos todas sus opciones de seguridad:
- cómo puede ejecutar servicios web con distintas cuentas,
- cómo implementar la escalada de privilegios, y
- cómo permitir que los usuarios normales invoquen tareas privilegiadas sin concederles derechos de administrador ni depender de incómodos «secretos».
- Por ahora, hemos mostrado datos estáticos en nuestro frontend de interfaz de usuario. En las próximas partes, veremos interfaces dinámicas que pueden actualizarse por sí mismas a medida que cambian los valores, y mucho más.

Tobias Weltner
Aleksandar Nikolić
Adam Driscoll
Steven Lafortune