Browse Source

Merge pull request #297 from Moonlight-Panel/AddLiveStatistics

Add live statistics
Marcel Baumgartner 1 year ago
parent
commit
71a8839f5f

+ 26 - 0
Moonlight/App/Helpers/Formatter.cs

@@ -156,6 +156,32 @@ public static class Formatter
             return (i / (1024D * 1024D)).Round(2) + " GB";
         }
     }
+    
+    public static double CalculateAverage(List<double> values)
+    {
+        if (values == null || values.Count == 0)
+        {
+            throw new ArgumentException("The list cannot be null or empty.");
+        }
+
+        double sum = 0;
+        foreach (double value in values)
+        {
+            sum += value;
+        }
+
+        return sum / values.Count;
+    }
+    
+    public static double CalculatePercentage(double part, double total)
+    {
+        if (total == 0)
+        {
+            return 0;
+        }
+
+        return (part / total) * 100;
+    }
 
     public static RenderFragment FormatLineBreaks(string content)
     {

+ 8 - 1
Moonlight/App/Perms/Permissions.cs

@@ -410,10 +410,17 @@ public static class Permissions
     
     public static Permission AdminChangelog = new()
     {
-        Index = 59,
+        Index = 60,
         Name = "Admin changelog",
         Description = "View the changelog"
     };
+    
+    public static Permission AdminStatisticsLive = new()
+    {
+        Index = 61,
+        Name = "Admin statistics live",
+        Description = "View the live statistics"
+    };
 
     public static Permission? FromString(string name)
     {

+ 4 - 2
Moonlight/Pages/_Layout.cshtml

@@ -1,4 +1,4 @@
-@using Microsoft.AspNetCore.Components.Web
+@using Microsoft.AspNetCore.Components.Web
 @using Moonlight.App.Extensions
 @using Moonlight.App.Repositories
 @using Moonlight.App.Services
@@ -48,7 +48,6 @@
     <link rel="stylesheet" type="text/css" href="/_content/XtermBlazor/XtermBlazor.css"/>
     <link rel="stylesheet" type="text/css" href="/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.css"/>
     <link rel="stylesheet" type="text/css" href="/_content/Blazor.ContextMenu/blazorContextMenu.min.css"/>
-
     <link rel="stylesheet" type="text/css" href="/assets/css/toastr.css"/>
     
     <meta name="viewport" content="width=device-width, initial-scale=1"/>
@@ -98,6 +97,9 @@
 <script src="/assets/js/popper.min.js"></script>
 <script src="/assets/js/bootstrap.min.js"></script>
 
+<script src="/_content/Blazor-ApexCharts/js/apex-charts.min.js"></script>
+<script src="/_content/Blazor-ApexCharts/js/blazor-apex-charts.js"></script>
+
 <script src="/_content/XtermBlazor/XtermBlazor.min.js"></script>
 <script src="/_content/BlazorTable/BlazorTable.min.js"></script>
 <script src="/_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js"></script>

+ 22 - 0
Moonlight/Shared/Components/Navigations/AdminStatisticsNavigation.razor

@@ -0,0 +1,22 @@
+<div class="card mb-5 mb-xl-10">
+    <div class="card-body pt-0 pb-0">
+        <ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
+            <li class="nav-item mt-2">
+                <a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/admin/statistics">
+                    <TL>Overview</TL>
+                </a>
+            </li>
+            <li class="nav-item mt-2">
+                <a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/statistics/live">
+                    <TL>Live data</TL>
+                </a>
+            </li>
+        </ul>
+    </div>
+</div>
+
+@code
+{
+    [Parameter]
+    public int Index { get; set; } = 0;
+}

+ 4 - 1
Moonlight/Shared/Views/Admin/Statistics.razor → Moonlight/Shared/Views/Admin/Statistics/Index.razor

@@ -6,13 +6,16 @@
 @using ApexCharts
 @using Moonlight.App.Helpers
 @using Moonlight.App.Services
+@using Moonlight.Shared.Components.Navigations
 
 @inject StatisticsViewService StatisticsViewService
 @inject SmartTranslateService SmartTranslateService
 
 @attribute [PermissionRequired(nameof(Permissions.AdminStatistics))]
 
-<div class="row mt-4 mb-2">
+<AdminStatisticsNavigation />
+
+<div class="row mb-2">
     <div class="col-12 col-lg-6 col-xl">
         <div class="card card-body">
             <select class="form-select" @bind="TimeSpanBind">

+ 204 - 0
Moonlight/Shared/Views/Admin/Statistics/Live.razor

@@ -0,0 +1,204 @@
+@page "/admin/statistics/live"
+
+@using Moonlight.App.Services
+@using Moonlight.App.Repositories
+@using Moonlight.App.Database.Entities
+@using Moonlight.App.Helpers
+@using Moonlight.App.Services.Sessions
+@using Moonlight.Shared.Components.Navigations
+
+@inject NodeService NodeService
+@inject Repository<Node> NodeRepository
+@inject IServiceScopeFactory ServiceScopeFactory
+
+@attribute [PermissionRequired(nameof(Permissions.AdminStatisticsLive))]
+
+<AdminStatisticsNavigation />
+
+<div class="row">
+    <div class="col-12 col-md-3 mb-3">
+        <div class="card">
+            <div class="card-header pt-5">
+                <div class="card-title d-flex flex-column">
+                    <span class="fs-2hx fw-bold text-white me-2 lh-1 ls-n2">@(Math.Round(TotalCpuUsed, 2))% / 100%</span>
+                    <span class="text-white opacity-75 pt-1 fw-semibold fs-6">
+                        <TL>Total cpu load</TL>
+                    </span>
+                </div>
+            </div>
+            <div class="card-body d-flex align-items-end pt-0">
+                <div class="d-flex align-items-center flex-column mt-3 w-100">
+                    @{
+                        var cpuPercent = Math.Round(Formatter.CalculatePercentage(TotalCpuUsed, 100));
+                    }
+
+                    <div class="d-flex justify-content-end fw-bold fs-6 text-white opacity-75 w-100 mt-auto mb-2">
+                        <span>@(cpuPercent)%</span>
+                    </div>
+
+                    <div class="h-8px mx-3 w-100 bg-white bg-opacity-50 rounded">
+                        <div class="bg-@(GetStateColor(cpuPercent)) rounded h-8px" role="progressbar" style="width: @(cpuPercent)%;" aria-valuenow="@(cpuPercent)" aria-valuemin="0" aria-valuemax="100"></div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="col-12 col-md-3 mb-3">
+        <div class="card">
+            <div class="card-header pt-5">
+                <div class="card-title d-flex flex-column">
+                    <span class="fs-2hx fw-bold text-white me-2 lh-1 ls-n2">@(ByteSizeValue.FromKiloBytes(TotalMemoryUsed).GigaBytes)GB / @(ByteSizeValue.FromKiloBytes(TotalMemory).GigaBytes)GB</span>
+                    <span class="text-white opacity-75 pt-1 fw-semibold fs-6">
+                        <TL>Total memory load</TL>
+                    </span>
+                </div>
+            </div>
+            <div class="card-body d-flex align-items-end pt-0">
+                <div class="d-flex align-items-center flex-column mt-3 w-100">
+                    @{
+                        var memoryPercent = Math.Round(Formatter.CalculatePercentage(TotalMemoryUsed, TotalMemory));
+                    }
+
+                    <div class="d-flex justify-content-end fw-bold fs-6 text-white opacity-75 w-100 mt-auto mb-2">
+                        <span>@(memoryPercent)%</span>
+                    </div>
+
+                    <div class="h-8px mx-3 w-100 bg-white bg-opacity-50 rounded">
+                        <div class="bg-@(GetStateColor(memoryPercent)) rounded h-8px" role="progressbar" style="width: @(memoryPercent)%;" aria-valuenow="@(memoryPercent)" aria-valuemin="0" aria-valuemax="100"></div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="col-12 col-md-3 mb-3">
+        <div class="card">
+            <div class="card-body pt-5">
+                <div class="card-title d-flex flex-column">
+                    <span class="fs-2hx fw-bold text-white me-2 lh-1 ls-n2">@(Users)</span>
+                    <span class="text-white opacity-75 pt-1 fw-semibold fs-6">
+                        <TL>Total user count</TL>
+                    </span>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="col-12 col-md-3 mb-3">
+        <div class="card">
+            <div class="card-body pt-5">
+                <div class="card-title d-flex flex-column">
+                    <span class="fs-2hx fw-bold text-white me-2 lh-1 ls-n2">@(Sessions)</span>
+                    <span class="text-white opacity-75 pt-1 fw-semibold fs-6">
+                        <TL>Total session count</TL>
+                    </span>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="col-12 col-md-3 mb-3">
+        <div class="card">
+            <div class="card-body pt-5">
+                <div class="card-title d-flex flex-column">
+                    <span class="fs-2hx fw-bold text-white me-2 lh-1 ls-n2">@(ActiveUsers)</span>
+                    <span class="text-white opacity-75 pt-1 fw-semibold fs-6">
+                        <TL>Total active user count</TL>
+                    </span>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+@code
+{
+    private long TotalMemoryUsed;
+    private long TotalMemory;
+
+    private double TotalCpuUsed;
+
+    private int Users;
+    private int ActiveUsers;
+    private int Sessions;
+
+    protected override Task OnAfterRenderAsync(bool firstRender)
+    {
+        if (firstRender)
+        {
+            Task.Run(async () =>
+            {
+                while (true)
+                {
+                    await Monitor();
+                    await Task.Delay(TimeSpan.FromSeconds(5));
+                }
+            });
+        }
+
+        return Task.CompletedTask;
+    }
+
+    private async Task Monitor()
+    {
+        async Task Nodes()
+        {
+            TotalMemory = 0;
+            TotalMemoryUsed = 0;
+
+            var cpuValues = new List<double>();
+
+            foreach (var node in NodeRepository.Get().ToArray())
+            {
+                try
+                {
+                    var metrics = await NodeService.GetMemoryMetrics(node);
+
+                    TotalMemory += metrics.Total;
+                    TotalMemoryUsed += metrics.Used;
+
+                    var cpuMetrics = await NodeService.GetCpuMetrics(node);
+                    cpuValues.Add(cpuMetrics.CpuUsage);
+                }
+                catch (Exception)
+                {
+    // ignored
+                }
+            }
+
+            TotalCpuUsed = Formatter.CalculateAverage(cpuValues);
+
+            await InvokeAsync(StateHasChanged);
+        }
+
+        async Task UsersAndSessions()
+        {
+            using var scope = ServiceScopeFactory.CreateScope();
+
+            var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
+            var sessionService = scope.ServiceProvider.GetRequiredService<SessionServerService>();
+
+            Users = userRepo.Get().Count();
+            Sessions = (await sessionService.GetSessions()).Length;
+            ActiveUsers = userRepo
+                .Get()
+                .Count(x => x.LastVisitedAt > DateTime.UtcNow.AddDays(-1));
+
+            await InvokeAsync(StateHasChanged);
+        }
+
+        await Nodes();
+        await UsersAndSessions();
+    }
+
+    private string GetStateColor(double percent)
+    {
+        if (percent < 60)
+            return "success";
+        else if (percent >= 60 && percent < 80)
+            return "warning";
+        else
+            return "danger";
+    }
+}