diff --git a/Blueberry.Redmine/RedmineApiClient.cs b/Blueberry.Redmine/RedmineApiClient.cs index bf3ecf7..1350621 100644 --- a/Blueberry.Redmine/RedmineApiClient.cs +++ b/Blueberry.Redmine/RedmineApiClient.cs @@ -1,5 +1,7 @@ using Blueberry.Redmine.Dto; using Microsoft.Extensions.Logging; +using System.ComponentModel.DataAnnotations; +using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -76,7 +78,7 @@ namespace Blueberry.Redmine throw new RedmineApiException("Redmine API Unreachable"); } - + /* private async Task> SendRequestWithPagingAsync(HttpMethod method, string endpoint, int limit, Func> itemParser, IProgress<(int current, int total)>? progress = null, object? payload = null, CancellationToken? token = null) where TResponse : IResponseList { @@ -84,10 +86,12 @@ namespace Blueberry.Redmine List returnList = []; + _logger.LogDebug("Starting paged request to {Endpoint} with limit {Limit}", endpoint, limit); + while (true) { var path = ""; - if(endpoint.Contains('?')) + if (endpoint.Contains('?')) path = $"{endpoint}&limit={limit}&offset={offset}"; else path = $"{endpoint}?limit={limit}&offset={offset}"; @@ -101,7 +105,7 @@ namespace Blueberry.Redmine break; offset += limit; - if(progress != null) + if (progress != null) { var current = Math.Min(offset + limit, responseList.TotalCount); progress.Report((current, responseList.TotalCount)); @@ -110,6 +114,92 @@ namespace Blueberry.Redmine return returnList; } + */ + private async Task> SendRequestWithPagingAsync(HttpMethod method, string endpoint, int limit, Func> itemParser, + IProgress<(int current, int total)>? progress = null, object? payload = null, CancellationToken? token = null) where TResponse : IResponseList + { + var offset = 0; + + List returnList = []; + + _logger.LogDebug("Starting paged request to {Endpoint} with limit {Limit}", endpoint, limit); + + var path = ""; + if (endpoint.Contains('?')) + path = $"{endpoint}&limit={limit}&offset={offset}"; + else + path = $"{endpoint}?limit={limit}&offset={offset}"; + + var responseList = await SendRequestAsync(HttpMethod.Get, path, token: token) + ?? throw new NullReferenceException(); + + var total = responseList.TotalCount; + + returnList.AddRange(itemParser(responseList)); + + offset += limit; + + if (offset >= responseList.TotalCount) + return returnList; + + var remain = total - offset; + + var tasks = new Task[(int)Math.Ceiling((double)remain / limit)]; + + _logger.LogDebug("Spawning {TaskCount} tasks for remaining {Remaining} items out of {Total}", tasks.Length, remain, total); + + List<(int id, IEnumerable items)> responses = []; + + for (int i = 0; i < tasks.Length; i++) + { + var id = i; + var o = offset; + tasks[i] = Task.Run(async () => + { + if (endpoint.Contains('?')) + path = $"{endpoint}&limit={limit}&offset={o}"; + else + path = $"{endpoint}?limit={limit}&offset={o}"; + + var responseList = await SendRequestAsync(HttpMethod.Get, path, token: token) + ?? throw new NullReferenceException(); + + responses.Add((id, itemParser(responseList))); + }); + + offset += limit; + } + + while (tasks.Any(t => !t.IsCompleted)) + { + if (progress != null) + { + var current = Math.Min(tasks.Count(x=>x.IsCompletedSuccessfully), tasks.Length); + progress.Report((current, tasks.Length)); + } + + var completed = tasks.Count(t => t.IsCompleted); + _logger.LogDebug("{Completed}/{Total} tasks completed", completed, tasks.Length); + await Task.Delay(250); + } + + await Task.WhenAll(tasks); + await Task.Delay(100); + + var notCompleted = tasks.Where(t => !t.IsCompletedSuccessfully).ToList(); + _logger.LogDebug("{NotCompleted} tasks did not complete successfully", notCompleted.Count); + + _logger.LogDebug("All tasks completed, aggregating {total} results from {count} responses", responses.Select(x=>x.items).Count(), responses.Count); + + foreach (var resp in responses.OrderBy(x => x.id)) + { + returnList.AddRange(resp.items); + } + + _logger.LogDebug("Aggregated total of {TotalItems} items", returnList.Count); + + return returnList; + } public async Task> GetStatusesAsync(CancellationToken? token = null) { @@ -364,7 +454,7 @@ namespace Blueberry.Redmine { var path = "users.json"; - return await SendRequestWithPagingAsync(HttpMethod.Get, path, limit, (x) => x.Users, progress, token); + return [.. (await SendRequestWithPagingAsync(HttpMethod.Get, path, limit, (x) => x.Users, progress, token)).OrderBy(x => x.FullName)]; } public async Task SetIssueStatusAsync(int issueId, int statusId, CancellationToken? token = null) diff --git a/Blueberry/App.xaml.cs b/Blueberry/App.xaml.cs index aacffcd..70e3f2d 100644 --- a/Blueberry/App.xaml.cs +++ b/Blueberry/App.xaml.cs @@ -2,6 +2,7 @@ using Blueberry.Redmine; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.IO; @@ -33,10 +34,16 @@ namespace BlueMine }); //services.AddTransient(); - services.AddHttpClient(client => client.BaseAddress = new Uri("http://localhost/")); + //services.AddHttpClient("client", client => client.BaseAddress = new Uri("http://localhost/")).RemoveAllLoggers(); + services.AddHttpClient("client").RemoveAllLoggers(); + services.Configure(opts => + { + opts.AddFilter("System.Net.Http.HttpClient", LogLevel.Warning); + }); // .AddHttpMessageHandler(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); }).Build(); diff --git a/Blueberry/HoursWindow.xaml.cs b/Blueberry/HoursWindow.xaml.cs index 57ba221..a5e0b31 100644 --- a/Blueberry/HoursWindow.xaml.cs +++ b/Blueberry/HoursWindow.xaml.cs @@ -1,9 +1,11 @@ using Blueberry.Redmine; using Blueberry.Redmine.Dto; +using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Collections.ObjectModel; using System.Windows; using Wpf.Ui.Controls; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace Blueberry { @@ -16,15 +18,17 @@ namespace Blueberry private readonly RedmineManager _manager; private readonly RedmineConfig _config; private readonly ConcurrentDictionary _issueNames = []; + private readonly ILogger _logger; public ObservableCollection Hours { get; set; } = []; - public HoursWindow(RedmineManager manager, RedmineConfig config) + public HoursWindow(RedmineManager manager, RedmineConfig config, ILogger logger) { InitializeComponent(); DataContext = this; _manager = manager; _config = config; + _logger = logger; } private async void FluentWindow_Loaded(object sender, RoutedEventArgs e) @@ -42,7 +46,7 @@ namespace Blueberry foreach (var user in u) userComboBox.Items.Add(user); - userComboBox.SelectedItem = current; + userComboBox.SelectedItem = _users.First(x=>x.Id == current.Id); userComboBox.IsEnabled = searchButton.IsEnabled = dateButton.IsEnabled = true; @@ -113,39 +117,66 @@ namespace Blueberry Task[] tasks = new Task[selectedDates.Count]; Hours.Clear(); hoursProgress.Visibility= Visibility.Visible; + var firstDate = userCalendar.SelectedDates.First(); + var lastDate = userCalendar.SelectedDates.Last(); - for (int i = 0; i < selectedDates.Count; i++) + var h = await _manager.GetTimeForUserAsync(user.Id, firstDate, lastDate); + + var list = new List(); + foreach (var item in h) + if (await GetIssueNameAsync(item.Issue.Id, download: false) == "") + list.Add(item.Issue.Id.ToString()); + + if (list.Count > 0) { - hoursProgress.IsIndeterminate = false; - totalI = selectedDates.Count; - var date = selectedDates[i]; - tasks[i] = Task.Run(async () => + list = [.. list.Distinct()]; + _logger.LogDebug("Downloading issue names for {count} issues", list.Count); + var ts = new Task[list.Count]; + for (int y = 0; y < list.Count; y++) { - var h = await _manager.GetTimeForUserAsync(user.Id, date, date); - foreach (var item in h) - { - var dh = new DisplayHours() - { - ProjectName = item.Project.Name, - IssueName = await GetIssueNameAsync(item.Issue.Id), - IssueId = item.Issue.Id.ToString(), - Date = date, - CreatedOn = item.CreatedOn, - Hours = item.Hours.ToString(), - Comments = item.Comments, - IsSeparator = false - }; - hours.Add(dh); - } - loadI++; - UpdateProgressI(); - }); - await Task.Delay(10); + string? id = list[y]; + ts[y] = GetIssueNameAsync(int.Parse(id)); + } + await Task.WhenAll(ts); } - await Task.WhenAll(tasks); + foreach (var item in h) + { + var dh = new DisplayHours() + { + ProjectName = item.Project.Name, + IssueName = await GetIssueNameAsync(item.Issue.Id), + IssueId = item.Issue.Id.ToString(), + Date = DateTime.Parse(item.SpentOn), + CreatedOn = item.CreatedOn, + Hours = item.Hours.ToString(), + Comments = item.Comments, + IsSeparator = false + }; + hours.Add(dh); + } - hoursProgress.IsIndeterminate = true; + var orderedHours = hours.OrderByDescending(h => h.CreatedOn).OrderBy(h => h.Date).ToList(); + var previousDate = DateTime.MinValue; + for (int i = 0; i < orderedHours.Count; i++) + { + if (orderedHours[i].Date.Date > previousDate) + { + var ttl = orderedHours.Where(x => x.Date.Date == orderedHours[i].Date.Date).Sum(x => double.Parse(x.Hours)); + var dh = new DisplayHours() + { + IsSeparator = true, + SeparatorText = orderedHours[i].Date.ToString("yyyy-MM-dd") + " | Összesen: " + ttl + " óra" + }; + orderedHours.Insert(i, dh); + previousDate = orderedHours[i + 1].Date.Date; + } + } + + hoursProgress.Visibility = Visibility.Hidden; + + foreach (var item in orderedHours) + Hours.Add(item); var newTickets = await _manager.GetIssuesAsync(user.Id, createdFrom: selectedDates.First(), createdTo: selectedDates.Last()); var closedTickets = await _manager.GetIssuesAsync(user.Id, isOpen: false, updatedFrom: selectedDates.First(), updatedTo: selectedDates.Last()); @@ -172,40 +203,23 @@ namespace Blueberry avgTicketAgeTextBlock.Text = Math.Round(currentTickets.Average(x => (DateTime.Now - x.CreatedOn).TotalDays), 2) + " nap"; var ages = currentTickets.Select(x => (DateTime.Now - x.CreatedOn).TotalDays).Order().ToList(); medianTicketAgeTextBlock.Text = Math.Round(ages[ages.Count / 2], 2) + " nap"; - - var orderedHours = hours.OrderByDescending(h => h.CreatedOn).OrderBy(h => h.Date).ToList(); - var previousDate = DateTime.MinValue; - for (int i = 0; i < orderedHours.Count; i++) - { - if(orderedHours[i].Date.Date > previousDate) - { - total = orderedHours.Where(x => x.Date.Date == orderedHours[i].Date.Date).Sum(x => double.Parse(x.Hours)); - var dh = new DisplayHours() - { - IsSeparator = true, - SeparatorText = orderedHours[i].Date.ToString("yyyy-MM-dd") + " | Összesen: " + total + " óra" - }; - orderedHours.Insert(i, dh); - previousDate = orderedHours[i+1].Date.Date; - } - } - - hoursProgress.Visibility = Visibility.Hidden; - - foreach (var item in orderedHours) - Hours.Add(item); } - private async Task GetIssueNameAsync(int issueId) + private async Task GetIssueNameAsync(int issueId, bool download = true) { try { if (_issueNames.ContainsKey(issueId)) return _issueNames[issueId]; - - var name = (await _manager.GetSimpleIssueAsync(issueId)).Subject; - _issueNames.TryAdd(issueId, name); - return name; + if(download) + { + var name = (await _manager.GetSimpleIssueAsync(issueId)).Subject; + _issueNames.TryAdd(issueId, name); + return name; + } else + { + return ""; + } } catch (Exception ex) { @@ -247,5 +261,6 @@ namespace Blueberry } } } + } } diff --git a/Blueberry/MainWindow.xaml b/Blueberry/MainWindow.xaml index a6f1c1a..7323f82 100644 --- a/Blueberry/MainWindow.xaml +++ b/Blueberry/MainWindow.xaml @@ -113,8 +113,11 @@ - + + + + - - - - - diff --git a/Blueberry/MainWindow.xaml.cs b/Blueberry/MainWindow.xaml.cs index 18c669b..97b0d16 100644 --- a/Blueberry/MainWindow.xaml.cs +++ b/Blueberry/MainWindow.xaml.cs @@ -1,6 +1,7 @@ using Blueberry; using Blueberry.Redmine; using Blueberry.Redmine.Dto; +using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.Diagnostics; using System.Text.RegularExpressions; @@ -19,16 +20,18 @@ namespace BlueMine private readonly RedmineSettingsManager _settings; private readonly RedmineConfig _config; private List _issues = []; + private readonly ILoggerFactory _loggerFactory; public ObservableCollection IssuesList { get; set; } = []; public ObservableCollection StatusList { get; set; } = []; - public MainWindow(RedmineManager manager, RedmineSettingsManager settings, RedmineConfig config) + public MainWindow(RedmineManager manager, RedmineSettingsManager settings, RedmineConfig config, ILoggerFactory loggerFactory) { _settings = settings; _config = config; _manager = manager; InitializeComponent(); DataContext = this; + _loggerFactory = loggerFactory; } private async void WindowLoaded(object sender, RoutedEventArgs e) @@ -368,7 +371,7 @@ namespace BlueMine private async void hoursButton_Click(object sender, RoutedEventArgs e) { - var hoursWindow = new HoursWindow(_manager, _config); + var hoursWindow = new HoursWindow(_manager, _config, _loggerFactory.CreateLogger()); hoursWindow.Show(); } diff --git a/Blueberry/UpdateManager.cs b/Blueberry/UpdateManager.cs index 73780f8..cf04fb5 100644 --- a/Blueberry/UpdateManager.cs +++ b/Blueberry/UpdateManager.cs @@ -8,7 +8,7 @@ namespace Blueberry public static class UpdateManager { private const string releaseUrl = "https://git.technopunk.space/api/v1/repos/tomi/Blueberry/releases/latest"; - public const string CurrentVersion = "0.2.2"; + public const string CurrentVersion = "0.2.5"; private static readonly string appDir = AppDomain.CurrentDomain.BaseDirectory; private static readonly string zipPath = Path.Combine(appDir, "blueberry_update.zip"); private static readonly HttpClient client = new();