diff --git a/Wabbajack.App.Wpf/App.xaml b/Wabbajack.App.Wpf/App.xaml
new file mode 100644
index 00000000..5b48b9c9
--- /dev/null
+++ b/Wabbajack.App.Wpf/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/Wabbajack.App.Wpf/App.xaml.cs b/Wabbajack.App.Wpf/App.xaml.cs
new file mode 100644
index 00000000..3561ed8d
--- /dev/null
+++ b/Wabbajack.App.Wpf/App.xaml.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace Wabbajack.App.Wpf
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application
+ {
+ }
+}
\ No newline at end of file
diff --git a/Wabbajack.App.Wpf/AssemblyInfo.cs b/Wabbajack.App.Wpf/AssemblyInfo.cs
new file mode 100644
index 00000000..4a05c7d4
--- /dev/null
+++ b/Wabbajack.App.Wpf/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
\ No newline at end of file
diff --git a/Wabbajack.App.Wpf/MainWindow.xaml b/Wabbajack.App.Wpf/MainWindow.xaml
new file mode 100644
index 00000000..b846ebca
--- /dev/null
+++ b/Wabbajack.App.Wpf/MainWindow.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/Wabbajack.App.Wpf/MainWindow.xaml.cs b/Wabbajack.App.Wpf/MainWindow.xaml.cs
new file mode 100644
index 00000000..9344fa1e
--- /dev/null
+++ b/Wabbajack.App.Wpf/MainWindow.xaml.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+
+namespace Wabbajack.App.Wpf
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window
+ {
+ public MainWindow()
+ {
+ InitializeComponent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Wabbajack.App.Wpf/Support/ViewModel.cs b/Wabbajack.App.Wpf/Support/ViewModel.cs
new file mode 100644
index 00000000..ddf42513
--- /dev/null
+++ b/Wabbajack.App.Wpf/Support/ViewModel.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Disposables;
+using System.Runtime.CompilerServices;
+using System.Text.Json.Serialization;
+using ReactiveUI;
+
+namespace Wabbajack.App.Wpf.Support;
+
+public class ViewModel : ReactiveObject, IDisposable
+{
+ private readonly Lazy _compositeDisposable = new();
+
+ [JsonIgnore]
+ public CompositeDisposable CompositeDisposable => _compositeDisposable.Value;
+
+ public virtual void Dispose()
+ {
+ if (_compositeDisposable.IsValueCreated)
+ {
+ _compositeDisposable.Value.Dispose();
+ }
+ }
+
+ protected void RaiseAndSetIfChanged(
+ ref T item,
+ T newItem,
+ [CallerMemberName] string? propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(item, newItem)) return;
+ item = newItem;
+ this.RaisePropertyChanged(propertyName);
+ }
+}
\ No newline at end of file
diff --git a/Wabbajack.App.Wpf/Wabbajack.App.Wpf.csproj b/Wabbajack.App.Wpf/Wabbajack.App.Wpf.csproj
new file mode 100644
index 00000000..6e4b47ef
--- /dev/null
+++ b/Wabbajack.App.Wpf/Wabbajack.App.Wpf.csproj
@@ -0,0 +1,19 @@
+
+
+
+ WinExe
+ net6.0-windows
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wabbajack.CLI/Verbs/GenerateMetricsReports.cs b/Wabbajack.CLI/Verbs/GenerateMetricsReports.cs
index ec329db8..bae58ef3 100644
--- a/Wabbajack.CLI/Verbs/GenerateMetricsReports.cs
+++ b/Wabbajack.CLI/Verbs/GenerateMetricsReports.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
+using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Wabbajack.Common;
@@ -36,9 +37,18 @@ public class GenerateMetricsReports : IVerb
private async Task Run(AbsolutePath output)
{
var subjects = await GetMetrics("one day ago", "now", "finish_install")
+ .Where(d => d.Action != d.Subject)
.Select(async d => d.GroupingSubject)
.ToHashSet();
+ var allTime = await GetMetrics("10 days ago", "now", "finish_install")
+ .Where(d => subjects.Contains(d.GroupingSubject))
+ .ToList();
+
+ var grouped = allTime.GroupBy(g => (g.Timestamp.ToString("yyyy-MM-dd"), g.GroupingSubject)).ToArray();
+
+
+
return 0;
}
diff --git a/Wabbajack.Common/DateTimeExtensions.cs b/Wabbajack.Common/DateTimeExtensions.cs
new file mode 100644
index 00000000..1745e684
--- /dev/null
+++ b/Wabbajack.Common/DateTimeExtensions.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Wabbajack.Common;
+
+public static class DateTimeExtensions
+{
+ public static DateTime TruncateToDate(this DateTime d)
+ {
+ return new DateTime(d.Year, d.Month, d.Day);
+ }
+
+}
\ No newline at end of file
diff --git a/Wabbajack.Server/Controllers/Metrics.cs b/Wabbajack.Server/Controllers/Metrics.cs
index 59d31d42..6e2792c4 100644
--- a/Wabbajack.Server/Controllers/Metrics.cs
+++ b/Wabbajack.Server/Controllers/Metrics.cs
@@ -1,13 +1,11 @@
-using System;
-using System.Linq;
-using System.Reflection;
+using System.Reflection;
using System.Text.Json;
-using System.Threading.Tasks;
using Chronic.Core;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Nettle;
using Wabbajack.Common;
+using Wabbajack.DTOs.ServerResponses;
using Wabbajack.Server.DataModels;
using Wabbajack.Server.DTOs;
@@ -86,8 +84,9 @@ public class MetricsController : ControllerBase
}
private static byte[] EOL = {(byte)'\n'};
+
[HttpGet]
- [Route("report")]
+ [Route("dump")]
public async Task GetMetrics([FromQuery] string action, [FromQuery] string from, [FromQuery] string? to)
{
var parser = new Parser();
@@ -106,6 +105,60 @@ public class MetricsController : ControllerBase
await Response.Body.WriteAsync(EOL);
}
}
+
+ [HttpGet]
+ [Route("report")]
+ public async Task GetReport([FromQuery] string action, [FromQuery] string from, [FromQuery] string? to)
+ {
+ var parser = new Parser();
+
+ to ??= "now";
+
+ var toDate = parser.Parse(to).Start!.Value.TruncateToDate();
+
+ var groupFilterStart = parser.Parse("three days ago").Start!.Value.TruncateToDate();
+ toDate = new DateTime(toDate.Year, toDate.Month, toDate.Day);
+
+ var prefetch = await _metricsStore.GetRecords(groupFilterStart, toDate, action)
+ .Where((Func) (d => d.Action != d.Subject))
+ .Select(async d => d.GroupingSubject)
+ .ToHashSet();;
+
+ var fromDate = parser.Parse(from).Start!.Value.TruncateToDate();
+
+ var counts = new Dictionary<(DateTime, string), long>();
+
+ await foreach (var record in _metricsStore.GetRecords(fromDate, toDate, action))
+ {
+ if (record.Subject == record.Action) continue;
+ if (!prefetch.Contains(record.GroupingSubject)) continue;
+
+ var key = (record.Timestamp.TruncateToDate(), record.GroupingSubject);
+ if (counts.TryGetValue(key, out var old))
+ counts[key] = old + 1;
+ else
+ counts[key] = 1;
+ }
+
+ Response.Headers.ContentType = "application/json";
+ var row = new Dictionary();
+ for (var d = fromDate; d <= toDate; d = d.AddDays(1))
+ {
+ row["_Timestamp"] = d;
+ foreach (var group in prefetch)
+ {
+ if (counts.TryGetValue((d, group), out var found))
+ row[group] = found;
+ else
+ row[group] = 0;
+ }
+ await JsonSerializer.SerializeAsync(Response.Body, row);
+ await Response.Body.WriteAsync(EOL);
+ }
+
+ }
+
+
public class Result
{
diff --git a/Wabbajack.Server/DataModels/Metrics.cs b/Wabbajack.Server/DataModels/Metrics.cs
index b6f79fa9..5838baf1 100644
--- a/Wabbajack.Server/DataModels/Metrics.cs
+++ b/Wabbajack.Server/DataModels/Metrics.cs
@@ -86,7 +86,8 @@ public class Metrics
{
try
{
- return groupingRegex.Match(metricSubject).Groups[0].ToString();
+ var result = groupingRegex.Match(metricSubject).Groups[0].ToString();
+ return string.IsNullOrEmpty(result) ? metricSubject : result;
}
catch (Exception)
{
diff --git a/Wabbajack.sln b/Wabbajack.sln
index f7f3c5e0..1a0dd5e2 100644
--- a/Wabbajack.sln
+++ b/Wabbajack.sln
@@ -116,6 +116,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.Downloaders.GameF
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.Launcher", "Wabbajack.Launcher\Wabbajack.Launcher.csproj", "{23D49FCC-A6CB-4873-879B-F90DA1871AA3}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.App.Wpf", "Wabbajack.App.Wpf\Wabbajack.App.Wpf.csproj", "{638F0057-8DF2-4702-BC13-AD599E921F71}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -322,6 +324,10 @@ Global
{23D49FCC-A6CB-4873-879B-F90DA1871AA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23D49FCC-A6CB-4873-879B-F90DA1871AA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23D49FCC-A6CB-4873-879B-F90DA1871AA3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {638F0057-8DF2-4702-BC13-AD599E921F71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {638F0057-8DF2-4702-BC13-AD599E921F71}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {638F0057-8DF2-4702-BC13-AD599E921F71}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {638F0057-8DF2-4702-BC13-AD599E921F71}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{4057B668-8595-44FE-9805-007B284A838F} = {98B731EE-4FC0-4482-A069-BCBA25497871}