MAIN MENU
Blog di Devolutions

Annunci, aggiornamenti e approfondimenti di Devolutions.

Thumbnail for Separare l'interfaccia utente e i dati in PSU: creare app ed endpoint nel modo corretto

Separare l'interfaccia utente e i dati in PSU: creare app ed endpoint nel modo corretto

Questo secondo articolo della serie su PSU mostra come separare l'interfaccia utente dai dati combinando App ed Endpoint in PowerShell Universal. Scopra come creare dashboard riutilizzabili, esporre i dati tramite servizi web e realizzare flussi di automazione più puliti e scalabili.

In questa seconda parte della serie, separeremo l’interfaccia utente dai dati. Questo consente di creare dashboard eleganti e potenti con poche righe di codice, sfruttando i servizi web per fornire i dati necessari.

Come vedrà alla fine, questo approccio offre un’enorme flessibilità e consente di affrontare con facilità sfide che normalmente causerebbero non pochi grattacapi.

Riepilogo veloce: le app PSU

Se ha seguito la parte 1, apprezzerà immediatamente il primo importante vantaggio offerto da PSU: funge da sistema di gestione e repository per il codice, consentendo di riutilizzare facilmente il lavoro precedente.

Ad esempio, la “lista dei servizi” creata nella parte 1 è ancora disponibile in PSU. Ogni volta che ha bisogno di maggiori informazioni sui servizi, può accedervi tramite la dashboard principale di PSU:

  • Faccia clic su Apps nella barra del menu verticale a sinistra.
  • Vedrà tutte le app. Individui il “Service Dashboard” creato nella parte 1.
  • Faccia clic sull’icona del globo per aprire l’app. Verranno visualizzati i servizi insieme ai dettagli aggiuntivi implementati, come il nome del gruppo di servizio e il nome del processo sottostante.

Ricordi che ogni app PSU è accessibile direttamente tramite il proprio URL univoco, quindi può aprirla accedendo al relativo link, ad esempio: http://localhost:5000/list-services/Home

Creazione di una dashboard di azione

Costruiremo una dashboard centrale con pulsanti di navigazione. Come qualsiasi elemento dotato di interfaccia utente, le dashboard sono semplicemente app PSU, quindi ripartiremo da dove avevamo interrotto la volta scorsa:

  • Faccia clic su “Apps” nel menu verticale principale a sinistra, quindi faccia clic sull’elemento secondario “Apps.” Verrà visualizzato un elenco delle app esistenti.
  • Faccia clic sul pulsante blu “Create App” in alto per aggiungere una nuova app. Scelga “Dashboard1” come nome e immetta “dashboard1” come URL. Quindi faccia clic su OK.
  • Faccia clic sull’icona della matita per aprire l’editor di codice. Sostituisca il codice di esempio con il codice seguente:
# 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
  • Faccia clic sull’icona Save (con il simbolo del dischetto). Questo passaggio è fondamentale: è necessario salvare manualmente le modifiche, altrimenti PSU le ignorerà.
  • Faccia clic sul pulsante “View App” per eseguirla. Verrà visualizzata l’interfaccia utente della dashboard, composta da tre pulsanti e da un menu hamburger generato automaticamente nell’angolo in alto a sinistra.

Questo esempio illustra alcuni concetti fondamentali, quindi esploriamo prima l’interfaccia e poi analizziamo i dettagli:

  • Facendo clic su “Services,” verrà visualizzata l’app dell’elenco servizi creata nella parte 1. Se compare un messaggio di errore, potrebbe aver saltato la parte 1, quindi l’app che elenca i servizi non esiste.
  • Facendo clic su “Processes” o “Something else,” si accede a pagine che fanno parte dell’app dashboard. Queste pagine non contengono ancora contenuti significativi.

Richiamare app esterne

Il pulsante “Services” dimostra come incorporare app esterne nella dashboard. Utilizzando Invoke-UDRedirect '/list-services' con il parametro -Native, è possibile specificare URL relativi alla radice principale di PSU e accedere a qualsiasi altra app creata in precedenza.

Sebbene sia possibile incorporare app esterne in questo modo, non è considerata una buona pratica perché ha un costo: si perde la navigazione automatica. Il menu hamburger nell’angolo in alto a sinistra non è più disponibile nell’app esterna. Questo è normale, poiché si sta effettivamente navigando verso un’app completamente diversa.

Richiamare pagine secondarie

Gli altri due pulsanti funzionano in modo diverso: portano a pagine secondarie dell’app dashboard. Utilizzano lo stesso cmdlet UDRedirect, ma senza il parametro -Native, quindi l’URL fa riferimento a una pagina secondaria interna creata all’interno della dashboard tramite New-UDPage.

Poiché queste pagine secondarie fanno parte dell’app dashboard, la navigazione automatica funziona come previsto e il menu hamburger rimane visibile indipendentemente dalla pagina attualmente visualizzata.

Poiché queste pagine secondarie vengono definite all’interno dell’app dashboard, si ha il pieno controllo e si possono aggiungere facilmente funzionalità dell’interfaccia utente in modo coerente, ad esempio aggiungendo un pulsante “Back to Home” più evidente.

Ecco un esempio. Basta sostituire il codice (e non dimentichi di fare clic sul pulsante Save prima di visualizzare l’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

Ora, nelle pagine secondarie, troverà un pulsante “Home” che consente di tornare comodamente alla radice della dashboard.


Pulsante Home

E il riutilizzo del codice?

L’uso di pagine secondarie comporta dei compromessi: non è possibile riutilizzare il lavoro investito nelle app precedenti. Perché le pagine secondarie mostrino contenuti utili, è necessario aggiungere personalmente il codice appropriato.

Questo ci porta verso strategie di progettazione migliori. Nella parte 1, abbiamo investito impegno nella creazione di elenchi di servizi completi e li abbiamo implementati come un’unica app. L’app ha funzionato come previsto, ma come si può notare ora, non è riutilizzabile. Un approccio migliore sarebbe stato separare dati e interfaccia utente in componenti distinti, ed è esattamente ciò che faremo oggi.

Nella parte 1, abbiamo introdotto le app PSU come livello dell’interfaccia utente (presentazione). Per completare il quadro, introdurremo ora gli endpoint PSU come livello dei dati.

Creazione di un endpoint (ovvero un servizio web)

Per rendere davvero riutilizzabili le informazioni sull’elenco dei servizi sviluppate nella parte 1, avremmo dovuto implementarle come servizio web invece che come app. Rimediamo ora:

  • Nel menu verticale della dashboard PSU, faccia clic su “APIs”, quindi su “Endpoints.” Verranno visualizzati tutti i servizi web autodefiniti (inizialmente nessuno). Faccia clic su “Create Endpoint.”
  • Nel campo “URL”, immetta get-service, quindi faccia clic su OK. Il servizio web verrà visualizzato nell’elenco.
  • Faccia clic sull’icona della matita per aprire l’editor di codice. Ora è possibile separare in modo chiaro l’interfaccia utente dai dati. Ecco il codice dei dati grezzi dalla parte 1:
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
  • Faccia clic sull’icona Save. Poi, in alto, faccia clic su “Test.” Successivamente, faccia clic su “Invoke” sul lato destro per eseguire il servizio web e verificare che funzioni. Dovrebbe vedere le informazioni sul servizio visualizzate in formato JSON.

Per impostazione predefinita, il servizio web creato è protetto e richiede l’autenticazione. Tratteremo l’autenticazione e le opzioni di sicurezza separatamente, quindi per ora disabiliteremo l’autenticazione:

  • Nell’elenco degli endpoint nella dashboard PSU, faccia clic sull’icona a forma di ingranaggio relativa al servizio web.
  • Nella finestra di dialogo, apra la scheda “Security” e deselezioni “Authentication.” Quindi faccia clic su OK.

Il servizio web è ora accessibile a chiunque, incluse le sessioni PowerShell esterne. Proviamolo aprendo una normale console PowerShell. Il servizio web dispone di un URL univoco, nel nostro esempio: http://localhost:5000/get-service. Basta avviare una qualsiasi console PowerShell ed eseguire:

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

Il servizio web restituisce i dati: missione compiuta, dati e interfaccia utente sono ora separati.


Servizio web PowerShell

Versione finale della dashboard

Ora che sa come separare chiaramente i dati dall’interfaccia utente, possiamo rivedere l’app dashboard e concentrarci interamente sull’interfaccia utente. Non dipendiamo più da app esterne; teniamo invece tutto focalizzato sull’interfaccia utente.

  • Nel menu principale della dashboard PSU, faccia clic su “Apps,” quindi sul sottomenu “Apps.” Sulla destra, vedrà le app PSU.
  • Faccia clic sull’icona della matita nell’app “Dashboard1” per aprire il codice. Lo sostituisca con il nuovo codice, quindi faccia clic sull’icona 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

Faccia clic su “View App” per provare questa versione finale. Vedrà gli stessi tre pulsanti, ma questa volta, facendo clic su “Services,” si passa senza interruzioni a una pagina secondaria che mostra l’elenco dei servizi migliorato. A questo punto, il menu hamburger è ancora disponibile, si ha accesso al pulsante “Home” e tutta la complessità legata alla creazione dell’elenco dei servizi è ora delegata a un servizio web.


Servizi

Prossima parte

Ora che abbiamo chiarito come separare in modo chiaro dati e interfaccia utente, ha visto due delle principali funzionalità di PSU: “Apps,” che fungono da livello di presentazione, e “Endpoints,” che sono essenzialmente servizi web che forniscono dati grezzi.

Nelle prossime parti, approfondiremo i vantaggi che PSU può offrire con questo approccio:

  • Per ora, abbiamo disabilitato completamente l’autenticazione. Nelle prossime parti, esploreremo tutte le opzioni di sicurezza disponibili:
    • come eseguire i servizi web con account diversi,
    • come implementare l’escalation dei privilegi, e
    • come consentire agli utenti normali di richiamare attività privilegiate senza concedere loro diritti di amministratore o dover ricorrere a scomodi “segreti”.
  • Per ora, abbiamo mostrato dati statici nel frontend dell’interfaccia utente. Nelle prossime parti, esamineremo interfacce utente dinamiche in grado di aggiornarsi automaticamente al variare dei valori, e molto altro ancora.