MAIN MENU
Devolutions Blog

Ankündigungen, Aktualisierungen und Einsichten von Devolutions

PowerShell
Thumbnail for Trennung von Nutzeroberfläche und Daten in PSU: Apps und Endpunkte richtig erstellen

Trennung von Nutzeroberfläche und Daten in PSU: Apps und Endpunkte richtig erstellen

Dieser zweite Artikel der PSU-Serie zeigt, wie Sie Nutzeroberfläche und Daten trennen können, indem Sie Apps und Endpunkte in PowerShell Universal kombinieren. Erfahren Sie, wie Sie wiederverwendbare Dashboards erstellen, Daten über Webdienste bereitstellen und übersichtlichere, skalierbarere Automatisierungs-Workflows entwickeln.

Im zweiten Teil dieser Serie trennen wir die Nutzeroberfläche von den Daten. So können Sie mit nur wenigen Codezeilen elegante und leistungsstarke Dashboards erstellen und gleichzeitig Webdienste nutzen, um die benötigten Daten bereitzustellen.

Wie Sie am Ende sehen werden, bietet dieser Ansatz enorme Flexibilität und ermöglicht es Ihnen, Herausforderungen mit Leichtigkeit zu bewältigen – Herausforderungen, die normalerweise viele Kopfschmerzen bereiten würden.

Kurze Zusammenfassung: PSU-Apps

Wenn Sie Teil 1 gelesen haben, werden Sie sofort den ersten wesentlichen Vorteil von PSU zu schätzen wissen: Es fungiert als Verwaltungssystem und Repository für Ihren Code und ermöglicht Ihnen so die einfache Wiederverwendung früherer Arbeiten.

Die in Teil 1 erstellte „Serviceliste“ ist beispielsweise weiterhin in PSU verfügbar. Wann immer Sie weitere Informationen zu Ihren Diensten benötigen, können Sie diese jederzeit über das PSU-Haupt-Dashboard abrufen:

Denken Sie daran, dass jede PSU-App direkt über ihre eindeutige URL erreichbar ist. Sie können sie also öffnen, indem Sie zu ihrer Verknüpfung navigieren, zum Beispiel: http://localhost:5000/list-services/Home

Erstellen eines Aktions-Dashboards

Wir erstellen ein zentrales Dashboard mit Navigationsschaltflächen. Wie bei allem, was eine Nutzeroberfläche hat, sind Dashboards einfach PSU-Apps, daher machen wir dort weiter, wo wir letztes Mal aufgehört haben:

# 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

Dieses Beispiel veranschaulicht einige Kernkonzepte. Sehen wir uns zunächst die Nutzeroberfläche an und betrachten wir anschließend die Details:

Aufruf externer Apps

Die Schaltfläche „Dienste“ zeigt, wie Sie externe Apps in Ihr Dashboard einbetten können. Mit Invoke-UDRedirect '/list-services' und dem Zusatz -Native können Sie URLs relativ zum PSU-Stammverzeichnis angeben und auf alle zuvor erstellten Apps zugreifen.

Es ist zwar möglich, externe Apps auf diese Weise einzubinden, doch gilt dies nicht als bewährte Vorgehensweise, da es einen Nachteil mit sich bringt: Die automatische Navigation geht verloren. Das Hamburger-Menü in der oberen linken Ecke ist in der externen App nicht mehr verfügbar. Dies ist zu erwarten, da man im Prinzip zu einer völlig anderen App navigiert.

Aufruf von Unterseiten

Die beiden anderen Schaltflächen funktionieren anders: Sie navigieren zu Unterseiten der Dashboard-App. Sie verwenden dasselbe UDRedirect-Cmdlet, jedoch ohne den Zusatz -Native, sodass die URL auf eine interne Unterseite verweist, die innerhalb Ihres Dashboards mit New-UDPage erstellt wurde.

Da diese untergeordneten Seiten Teil Ihrer Dashboard-App sind, funktioniert die automatische Navigation wie erwartet und das Hamburger-Menü bleibt sichtbar, unabhängig davon, welche Seite gerade angezeigt wird.

Da Sie diese Unterseiten innerhalb Ihrer Dashboard-App definieren, haben Sie die volle Kontrolle und können ganz einfach und einheitlich neue UI-Funktionen hinzufügen – z. B. durch Einfügen einer besser sichtbaren Schaltfläche „Zurück zur Startseite“.

Hier ist ein Beispiel. Ersetzen Sie einfach Ihren Code (und vergessen Sie nicht, vor dem Anzeigen der App auf die Schaltfläche Speichern zu klicken):

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

Auf den Unterseiten finden Sie nun eine Schaltfläche „Startseite“, die Sie bequem zurück zum Dashboard-Startbildschirm führt.


Home-Schaltfläche

Wie sieht es mit der Wiederverwendung von Code aus?

Die Verwendung von Unterseiten bringt Nachteile mit sich: Die in vorigen Apps geleistete Arbeit kann nicht wiederverwendet werden. Damit Unterseiten nützliche Inhalte anzeigen, müssen Sie den entsprechenden Code selbst hinzufügen.

Dies führt uns zu besseren Designstrategien. In Teil 1 haben wir viel Aufwand in die Erstellung umfassender Dienstlisten investiert und diese in einer einzigen App implementiert. Die App funktionierte wie erwartet, ist aber, wie Sie jetzt sehen, nicht wiederverwendbar. Ein besserer Ansatz wäre gewesen, Daten und Nutzeroberfläche in separate Komponenten aufzuteilen – und genau das werden wir heute tun.

In Teil 1 haben wir PSU-Apps als UI-Ebene (Darstellungsebene) vorgestellt. Um das Bild zu vervollständigen, werden wir nun PSU-Endpunkte als Datenebene einführen.

Erstellung eines Endpunkts (auch Webdienst genannt)

Um die in Teil 1 entwickelten Informationen zur Dienstliste wirklich wiederverwendbar zu machen, hätten wir sie als Webdienst anstatt als App implementieren sollen. Das werden wir nun korrigieren:

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

Standardmäßig ist Ihr erstellter Webdienst gesichert und erfordert eine Authentifizierung. Wir werden die Authentifizierungs- und Sicherheitsoptionen separat behandeln und deaktivieren die Authentifizierung deshalb zunächst:

Ihr Webdienst ist nun für alle zugänglich, auch für externe PowerShell-Sitzungen. Testen wir ihn, indem wir eine normale PowerShell-Konsole öffnen. Ihr Webdienst hat eine eindeutige URL – in unserem Beispiel: http://localhost:5000/get-service. Starten Sie einfach eine beliebige PowerShell-Konsole und führen Sie folgenden Befehl aus:

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

Der Webdienst gibt die Daten an Sie zurück – Mission erfüllt: Daten und Nutzeroberfläche sind nun getrennt.


Webdienst PowerShell

Finale Dashboard-Version

Nachdem Sie nun wissen, wie man Daten klar von der Nutzeroberfläche trennt, können wir uns erneut unserer Dashboard-App zuwenden und uns ganz auf die Nutzeroberfläche konzentrieren. Wir sind nun nicht mehr auf externe Apps angewiesen, sondern richten alles auf die Nutzeroberfläche aus.

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

Klicken Sie auf „App anzeigen“, um diese finale Version zu testen. Sie werden dieselben drei Schaltflächen sehen, aber wenn Sie dieses Mal auf „Dienste“ klicken, gelangen Sie direkt zu einer Unterseite mit der erweiterten Dienstliste. Zu diesem Zeitpunkt ist das Hamburger-Menü weiterhin verfügbar, Sie haben Zugriff auf die Schaltfläche „Startseite“, und die gesamte Komplexität der Erstellung der Dienstliste wird nun an einen Webdienst ausgelagert.


Dienste

Nächster Teil

Nachdem wir nun geklärt haben, wie man Daten sauber von der Nutzeroberfläche trennen kann, haben Sie zwei der Hauptfunktionen von PSU kennengelernt: „Apps“, die als Präsentationsebene dienen, und „Endpunkte“, bei denen es sich im Wesentlichen um Webdienste handelt, die Rohdaten bereitstellen.

In den nächsten Teilen werden wir näher auf die Vorteile eingehen, die PSU mit diesem Ansatz bieten kann:

More from PowerShell

Read more articles