diff --git a/SystemdCtl.Client/Pages/ServiceManage.razor b/SystemdCtl.Client/Pages/ServiceManage.razor
new file mode 100644
index 0000000..9a32087
--- /dev/null
+++ b/SystemdCtl.Client/Pages/ServiceManage.razor
@@ -0,0 +1,66 @@
+@page "/Service/{ServiceName}/Manage"
+@using LibSystemdCli.Models
+@using LibSystemdCli
+@using System.Text.RegularExpressions
+@using SystemdCtl.Client.Abstractions
+@* @attribute [StreamRendering] *@
+@rendermode InteractiveWebAssembly
+@inject NavigationManager NavigationManager
+
+
+<PageTitle>Manage @ServiceName</PageTitle>
+
+<h1>Manage @ServiceName</h1>
+
+@* //simple log view *@
+<div class="row">
+ <div class="col-12">
+ <h3>Logs</h3>
+ <div class="card">
+ <div class="card-body">
+ <pre>
+ @foreach (var line in LogLines) {
+ <span>@line</span><br/>
+ }
+ </pre>
+ </div>
+ </div>
+ </div>
+</div>
+
+@code {
+
+ [Parameter]
+ public string ServiceName { get; set; } = "";
+
+ private static bool IsClient => !Environment.CommandLine.Contains("/");
+
+ private List<string> LogLines { get; set; } = new();
+
+ protected override async Task OnInitializedAsync() {
+ Console.WriteLine("OnInitializedAsync");
+ await Run();
+ }
+
+ private async Task Run() {
+ if (!IsClient) return;
+
+ LogLines.Clear();
+ var Http = new StreamingHttpClient() { BaseAddress = new Uri(NavigationManager.BaseUri) };
+ var _items = Http.GetAsyncEnumerableFromJsonAsync<string>($"/api/unit/{ServiceName}/logs");
+ await foreach (var item in _items) {
+ LogLines.Add(item);
+ if (LogLines.Count > 100) LogLines.RemoveAt(0);
+ StateHasChanged();
+ }
+ }
+
+ private string Capitalize(string input) {
+ return input switch {
+ null => throw new ArgumentNullException(nameof(input)),
+ "" => throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)),
+ _ => input.First().ToString().ToUpper() + input[1..]
+ };
+ }
+
+}
\ No newline at end of file
diff --git a/SystemdCtl.Client/Pages/Services.razor b/SystemdCtl.Client/Pages/Services.razor
new file mode 100644
index 0000000..d0f67a7
--- /dev/null
+++ b/SystemdCtl.Client/Pages/Services.razor
@@ -0,0 +1,154 @@
+@page "/Services"
+@using LibSystemdCli.Models
+@using System.Text.RegularExpressions
+@using SystemdCtl.Client.Abstractions
+@using ArcaneLibs.Blazor.Components
+@* @attribute [StreamRendering] *@
+@rendermode InteractiveWebAssembly
+@inject NavigationManager NavigationManager
+
+
+<PageTitle>Services</PageTitle>
+
+<h1>Services</h1>
+
+<span>
+ <label>Type: </label>
+ <InputSelect @bind-Value="TypeFilter">
+ <option value="">All</option>
+ @foreach (var i in UnitTypes) {
+ <option value="@i">@Capitalize(i)</option>
+ }
+ </InputSelect>
+</span>
+<span>
+ <InputCheckbox @bind-Value="@ShowSystem"></InputCheckbox>
+ <label>Show system services</label>
+</span>
+
+@if (filteredItems is not { Count: > 0 }) {
+ <p>
+ <em>Loading...</em>
+ </p>
+}
+else {
+ <table class="table">
+ <thead>
+ <tr>
+ <th>Service</th>
+ <th>Description</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var unit in filteredItems) {
+ <tr>
+ <td>@unit.Unit</td>
+ <td>@unit.Description</td>
+ <td>@unit.Active</td>
+ <td><LinkButton href="@($"/Service/{unit.Unit}/Manage")">Manage</LinkButton></td>
+ </tr>
+ @foreach (var frag in unit.FragmentPaths) {
+ <tr>
+ <td/>
+ <td>@frag</td>
+ <td/>
+ </tr>
+ }
+ }
+ </tbody>
+ </table>
+}
+
+@code {
+
+ private static Regex[] AlwaysHidden = new Regex[] {
+ //services
+ new Regex(@"^systemd-fsck@.*\.service$"),
+ new Regex(@"^modprobe@.*\.service$"),
+ new Regex(@"^xen.*\.service$"),
+ new Regex(@"^virt.*\.service$"),
+ new Regex(@"^libvirt.*\.service$"),
+ new Regex(@"^systemd-.*\.service$"),
+ //sockets
+ new Regex(@"^virt.*\.socket$"),
+ new Regex(@"^systemd.*\.socket$"),
+ new Regex(@"^libvirt.*\.socket$"),
+ //device
+ new(@"^dev-disk-by.*\.device$"),
+ new(@"^sys-device.*\.device$"),
+ new(@".*-by\\x2d.*\.device$"),
+ //mount
+ new(@"^run-credentials.*\.mount"),
+ //target
+ new(@"^blockdev@dev-disk-by.*\.target$")
+ };
+
+ private static bool IsClient => !Environment.CommandLine.Contains("/");
+ private List<SystemdUnitListItem>? items = new();
+ private List<SystemdUnitListItem>? filteredItems = new();
+ private List<string> UnitTypes = new();
+ private string _typeFilter = "";
+ private bool _showSystem;
+
+ public string TypeFilter {
+ get => _typeFilter;
+ set {
+ _typeFilter = value;
+ FilterItems();
+ }
+ }
+
+ public bool ShowSystem {
+ get => _showSystem;
+ set {
+ _showSystem = value;
+ FilterItems();
+ }
+ }
+
+ protected override async Task OnInitializedAsync() {
+ // await Task.Delay(500);
+
+ // items = await SystemdExecutor.GetUnits();
+ Console.WriteLine("OnInitializedAsync");
+ await ReloadItems();
+ }
+
+ private async Task ReloadItems() {
+ if (!IsClient) return;
+
+ items.Clear();
+ var Http = new StreamingHttpClient() { BaseAddress = new Uri(NavigationManager.BaseUri) };
+ var _items = Http.GetAsyncEnumerableFromJsonAsync<SystemdUnitListItem>("/api/listUnits");
+ await foreach (var item in _items) {
+ items.Add(item);
+ if (items.Count % 10 == 0)
+ await FilterItems();
+ }
+
+ await FilterItems();
+ }
+
+ private async Task FilterItems() {
+ var filter = items.Where(x => true);//!AlwaysHidden.Any(y => y.IsMatch(x.Unit)));
+ if (!_showSystem)
+ filter = filter.Where(x => !x.IsSystem);
+
+ UnitTypes = filter.Select(x => x.UnitType).Distinct().ToList();
+ filter = filter.Where(x => x.Unit.EndsWith(TypeFilter)).ToList();
+
+ filteredItems = filter.ToList();
+ StateHasChanged();
+ }
+
+ private string Capitalize(string input) {
+ return input switch {
+ null => throw new ArgumentNullException(nameof(input)),
+ "" => throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)),
+ _ => input.First().ToString().ToUpper() + input[1..]
+ };
+ }
+
+}
\ No newline at end of file
|