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}