From aa8d4cfe462572360079906e739967ef06e0b737 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 27 Mar 2024 08:52:27 +0800 Subject: [PATCH] Time tooltips show full time precision --- .../Controls/Chart/MetricTable.razor | 2 +- .../Controls/StructuredLogDetails.razor | 4 +- .../Components/Pages/Resources.razor | 2 +- .../Components/Pages/StructuredLogs.razor | 4 +- .../Components/Pages/TraceDetail.razor | 2 +- .../Components/Pages/Traces.razor | 4 +- src/Aspire.Dashboard/Utils/FormatHelpers.cs | 71 +++++++++++++------ .../FormatHelpersTests.cs | 33 +++++---- 8 files changed, 76 insertions(+), 46 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor b/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor index 5ca5a427b0..a8849fb087 100644 --- a/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor +++ b/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor @@ -16,7 +16,7 @@
- + @FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, TimeProvider.ToLocal(context.DateTime)) diff --git a/src/Aspire.Dashboard/Components/Controls/StructuredLogDetails.razor b/src/Aspire.Dashboard/Components/Controls/StructuredLogDetails.razor index e62d98be89..d6169bc5e7 100644 --- a/src/Aspire.Dashboard/Components/Controls/StructuredLogDetails.razor +++ b/src/Aspire.Dashboard/Components/Controls/StructuredLogDetails.razor @@ -10,8 +10,8 @@ @((MarkupString)string.Format(ControlsStrings.StructuredLogsDetailsResource, ViewModel.LogEntry.Application.ApplicationName))
-
- @((MarkupString)string.Format(ControlsStrings.StructuredLogsDetailsTimestamp, FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, ViewModel.LogEntry.TimeStamp, includeMilliseconds: true))) +
+ @((MarkupString)string.Format(ControlsStrings.StructuredLogsDetailsTimestamp, FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, ViewModel.LogEntry.TimeStamp, MillisecondsDisplay.Truncated)))
- + diff --git a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor index 8ae4ed60a4..9e9ab96294 100644 --- a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor +++ b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor @@ -87,8 +87,8 @@ - - @FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, context.TimeStamp, includeMilliseconds: true) + + @FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, context.TimeStamp, MillisecondsDisplay.Truncated) diff --git a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor index ba446eaabe..9287f6ba5c 100644 --- a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor +++ b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor @@ -19,7 +19,7 @@
- @Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailTraceStartHeader)] @FormatHelpers.FormatDateTime(TimeProvider, _trace.FirstSpan.StartTime, includeMilliseconds: true) + @Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailTraceStartHeader)] @FormatHelpers.FormatDateTime(TimeProvider, _trace.FirstSpan.StartTime, MillisecondsDisplay.Truncated)
diff --git a/src/Aspire.Dashboard/Components/Pages/Traces.razor b/src/Aspire.Dashboard/Components/Pages/Traces.razor index 57cca762ee..756b41dd24 100644 --- a/src/Aspire.Dashboard/Components/Pages/Traces.razor +++ b/src/Aspire.Dashboard/Components/Pages/Traces.razor @@ -31,8 +31,8 @@
- - @FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, context.FirstSpan.StartTime, includeMilliseconds: true) + + @FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, context.FirstSpan.StartTime, MillisecondsDisplay.Truncated) diff --git a/src/Aspire.Dashboard/Utils/FormatHelpers.cs b/src/Aspire.Dashboard/Utils/FormatHelpers.cs index 356936ead1..62727e4571 100644 --- a/src/Aspire.Dashboard/Utils/FormatHelpers.cs +++ b/src/Aspire.Dashboard/Utils/FormatHelpers.cs @@ -9,13 +9,21 @@ namespace Aspire.Dashboard.Utils; +public enum MillisecondsDisplay +{ + None, + Truncated, + Full +} + internal static partial class FormatHelpers { // There are an unbound number of CultureInfo instances so we don't want to use it as the key. // Someone could have also customized their culture so we don't want to use the name as the key. // This struct contains required information from the culture that is used in cached format strings. private readonly record struct CultureDetailsKey(string LongTimePattern, string ShortDatePattern, string NumberDecimalSeparator); - private sealed record MillisecondFormatStrings(string LongTimePattern, string ShortDateLongTimePattern); + private sealed record MillisecondFormatStrings(MillisecondFormatString LongTimePattern, MillisecondFormatString ShortDateLongTimePattern); + private sealed record MillisecondFormatString(string TruncatedMilliseconds, string FullMilliseconds); private static readonly ConcurrentDictionary s_formatStrings = new(); [GeneratedRegex("(:ss|:s)")] @@ -27,53 +35,72 @@ private static MillisecondFormatStrings GetMillisecondFormatStrings(CultureInfo return s_formatStrings.GetOrAdd(key, static k => { - var longTimePatternWithMilliseconds = GetLongTimePatternWithMillisecondsCore(k); - return new MillisecondFormatStrings(longTimePatternWithMilliseconds, k.ShortDatePattern + " " + longTimePatternWithMilliseconds); + var (truncated, full) = GetLongTimePatternWithMillisecondsCore(k); + return new MillisecondFormatStrings( + new MillisecondFormatString(truncated, full), + new MillisecondFormatString( + k.ShortDatePattern + " " + truncated, + k.ShortDatePattern + " " + full)); }); - static string GetLongTimePatternWithMillisecondsCore(CultureDetailsKey key) + static (string Truncated, string Full) GetLongTimePatternWithMillisecondsCore(CultureDetailsKey key) { // From https://learn.microsoft.com/dotnet/standard/base-types/how-to-display-milliseconds-in-date-and-time-values - // Gets the long time pattern, which is something like "h:mm:ss tt" (en-US), "H:mm:ss" (ja-JP), "HH:mm:ss" (fr-FR). - var longTimePattern = key.LongTimePattern; - // Create a format similar to .fff but based on the current culture. // Intentionally use fff here instead of FFF so output has a consistent length. - var millisecondFormat = $"'{key.NumberDecimalSeparator}'fff"; + var truncatedMillisecondFormat = "fff"; // Append millisecond pattern to current culture's long time pattern. - return MatchSecondsInTimeFormatPattern().Replace(longTimePattern, $"$1{millisecondFormat}"); + return ( + FormatPattern(key, truncatedMillisecondFormat), + FormatPattern(key, "FFFFFFF")); + } + + static string FormatPattern(CultureDetailsKey key, string millisecondFormat) + { + // Gets the long time pattern, which is something like "h:mm:ss tt" (en-US), "H:mm:ss" (ja-JP), "HH:mm:ss" (fr-FR). + var longTimePattern = key.LongTimePattern; + + return MatchSecondsInTimeFormatPattern().Replace(longTimePattern, $"$1'{key.NumberDecimalSeparator}'{millisecondFormat}"); } } - private static string GetLongTimePatternWithMilliseconds(CultureInfo cultureInfo) => GetMillisecondFormatStrings(cultureInfo).LongTimePattern; + private static MillisecondFormatString GetLongTimePatternWithMilliseconds(CultureInfo cultureInfo) => GetMillisecondFormatStrings(cultureInfo).LongTimePattern; - private static string GetShortDateLongTimePatternWithMilliseconds(CultureInfo cultureInfo) => GetMillisecondFormatStrings(cultureInfo).ShortDateLongTimePattern; + private static MillisecondFormatString GetShortDateLongTimePatternWithMilliseconds(CultureInfo cultureInfo) => GetMillisecondFormatStrings(cultureInfo).ShortDateLongTimePattern; - public static string FormatTime(BrowserTimeProvider timeProvider, DateTime value, bool includeMilliseconds = false, CultureInfo? cultureInfo = null) + public static string FormatTime(BrowserTimeProvider timeProvider, DateTime value, MillisecondsDisplay millisecondsDisplay = MillisecondsDisplay.None, CultureInfo? cultureInfo = null) { cultureInfo ??= CultureInfo.CurrentCulture; var local = timeProvider.ToLocal(value); // Long time - return includeMilliseconds - ? local.ToString(GetLongTimePatternWithMilliseconds(cultureInfo), cultureInfo) - : local.ToString("T", cultureInfo); + return millisecondsDisplay switch + { + MillisecondsDisplay.None => local.ToString("T", cultureInfo), + MillisecondsDisplay.Truncated => local.ToString(GetLongTimePatternWithMilliseconds(cultureInfo).TruncatedMilliseconds, cultureInfo), + MillisecondsDisplay.Full => local.ToString(GetLongTimePatternWithMilliseconds(cultureInfo).FullMilliseconds, cultureInfo), + _ => throw new NotImplementedException() + }; } - public static string FormatDateTime(BrowserTimeProvider timeProvider, DateTime value, bool includeMilliseconds = false, CultureInfo? cultureInfo = null) + public static string FormatDateTime(BrowserTimeProvider timeProvider, DateTime value, MillisecondsDisplay millisecondsDisplay = MillisecondsDisplay.None, CultureInfo? cultureInfo = null) { cultureInfo ??= CultureInfo.CurrentCulture; var local = timeProvider.ToLocal(value); // Short date, long time - return includeMilliseconds - ? local.ToString(GetShortDateLongTimePatternWithMilliseconds(cultureInfo), cultureInfo) - : local.ToString("G", cultureInfo); + return millisecondsDisplay switch + { + MillisecondsDisplay.None => local.ToString("G", cultureInfo), + MillisecondsDisplay.Truncated => local.ToString(GetShortDateLongTimePatternWithMilliseconds(cultureInfo).TruncatedMilliseconds, cultureInfo), + MillisecondsDisplay.Full => local.ToString(GetShortDateLongTimePatternWithMilliseconds(cultureInfo).FullMilliseconds, cultureInfo), + _ => throw new NotImplementedException() + }; } - public static string FormatTimeWithOptionalDate(BrowserTimeProvider timeProvider, DateTime value, bool includeMilliseconds = false, CultureInfo? cultureInfo = null) + public static string FormatTimeWithOptionalDate(BrowserTimeProvider timeProvider, DateTime value, MillisecondsDisplay millisecondsDisplay = MillisecondsDisplay.None, CultureInfo? cultureInfo = null) { var local = timeProvider.ToLocal(value); @@ -82,12 +109,12 @@ public static string FormatTimeWithOptionalDate(BrowserTimeProvider timeProvider { // e.g. "08:57:44" (based on user's culture and preferences) // Don't include milliseconds as resource server returned time stamp is second precision. - return FormatTime(timeProvider, local, includeMilliseconds, cultureInfo); + return FormatTime(timeProvider, local, millisecondsDisplay, cultureInfo); } else { // e.g. "9/02/2024 08:57:44" (based on user's culture and preferences) - return FormatDateTime(timeProvider, local, includeMilliseconds, cultureInfo); + return FormatDateTime(timeProvider, local, millisecondsDisplay, cultureInfo); } } diff --git a/tests/Aspire.Dashboard.Tests/FormatHelpersTests.cs b/tests/Aspire.Dashboard.Tests/FormatHelpersTests.cs index c694838f9d..a998a414d8 100644 --- a/tests/Aspire.Dashboard.Tests/FormatHelpersTests.cs +++ b/tests/Aspire.Dashboard.Tests/FormatHelpersTests.cs @@ -34,33 +34,36 @@ public void FormatNumberWithOptionalDecimalPlaces_GermanCulture(string expected, } [Theory] - [InlineData("06/15/2009 13:45:30.000", true, "2009-06-15T13:45:30.0000000Z")] - [InlineData("06/15/2009 13:45:30.123", true, "2009-06-15T13:45:30.1234567Z")] - [InlineData("06/15/2009 13:45:30", false, "2009-06-15T13:45:30.0000000Z")] - [InlineData("06/15/2009 13:45:30", false, "2009-06-15T13:45:30.1234567Z")] - public void FormatDateTime_WithMilliseconds_InvariantCulture(string expected, bool includeMilliseconds, string value) + [InlineData("06/15/2009 13:45:30.000", MillisecondsDisplay.Truncated, "2009-06-15T13:45:30.0000000Z")] + [InlineData("06/15/2009 13:45:30.123", MillisecondsDisplay.Truncated, "2009-06-15T13:45:30.1234567Z")] + [InlineData("06/15/2009 13:45:30.1234567", MillisecondsDisplay.Full, "2009-06-15T13:45:30.1234567Z")] + [InlineData("06/15/2009 13:45:30", MillisecondsDisplay.None, "2009-06-15T13:45:30.0000000Z")] + [InlineData("06/15/2009 13:45:30", MillisecondsDisplay.None, "2009-06-15T13:45:30.1234567Z")] + public void FormatDateTime_WithMilliseconds_InvariantCulture(string expected, MillisecondsDisplay includeMilliseconds, string value) { var date = GetLocalDateTime(value); Assert.Equal(expected, FormatHelpers.FormatDateTime(CreateTimeProvider(), date, includeMilliseconds, cultureInfo: CultureInfo.InvariantCulture)); } [Theory] - [InlineData("15.06.2009 13:45:30,000", true, "2009-06-15T13:45:30.0000000Z")] - [InlineData("15.06.2009 13:45:30,123", true, "2009-06-15T13:45:30.1234567Z")] - [InlineData("15.06.2009 13:45:30", false, "2009-06-15T13:45:30.0000000Z")] - [InlineData("15.06.2009 13:45:30", false, "2009-06-15T13:45:30.1234567Z")] - public void FormatDateTime_WithMilliseconds_GermanCulture(string expected, bool includeMilliseconds, string value) + [InlineData("15.06.2009 13:45:30,000", MillisecondsDisplay.Truncated, "2009-06-15T13:45:30.0000000Z")] + [InlineData("15.06.2009 13:45:30,123", MillisecondsDisplay.Truncated, "2009-06-15T13:45:30.1234567Z")] + [InlineData("15.06.2009 13:45:30,1234567", MillisecondsDisplay.Full, "2009-06-15T13:45:30.1234567Z")] + [InlineData("15.06.2009 13:45:30", MillisecondsDisplay.None, "2009-06-15T13:45:30.0000000Z")] + [InlineData("15.06.2009 13:45:30", MillisecondsDisplay.None, "2009-06-15T13:45:30.1234567Z")] + public void FormatDateTime_WithMilliseconds_GermanCulture(string expected, MillisecondsDisplay includeMilliseconds, string value) { var date = GetLocalDateTime(value); Assert.Equal(expected, FormatHelpers.FormatDateTime(CreateTimeProvider(), date, includeMilliseconds, cultureInfo: CultureInfo.GetCultureInfo("de-DE"))); } [Theory] - [InlineData("15/06/2009 1:45:30.000 pm", true, "2009-06-15T13:45:30.0000000Z")] - [InlineData("15/06/2009 1:45:30.123 pm", true, "2009-06-15T13:45:30.1234567Z")] - [InlineData("15/06/2009 1:45:30 pm", false, "2009-06-15T13:45:30.0000000Z")] - [InlineData("15/06/2009 1:45:30 pm", false, "2009-06-15T13:45:30.1234567Z")] - public void FormatDateTime_WithMilliseconds_NewZealandCulture(string expected, bool includeMilliseconds, string value) + [InlineData("15/06/2009 1:45:30.000 pm", MillisecondsDisplay.Truncated, "2009-06-15T13:45:30.0000000Z")] + [InlineData("15/06/2009 1:45:30.123 pm", MillisecondsDisplay.Truncated, "2009-06-15T13:45:30.1234567Z")] + [InlineData("15/06/2009 1:45:30.1234567 pm", MillisecondsDisplay.Full, "2009-06-15T13:45:30.1234567Z")] + [InlineData("15/06/2009 1:45:30 pm", MillisecondsDisplay.None, "2009-06-15T13:45:30.0000000Z")] + [InlineData("15/06/2009 1:45:30 pm", MillisecondsDisplay.None, "2009-06-15T13:45:30.1234567Z")] + public void FormatDateTime_WithMilliseconds_NewZealandCulture(string expected, MillisecondsDisplay includeMilliseconds, string value) { var date = GetLocalDateTime(value); Assert.Equal(expected, FormatHelpers.FormatDateTime(CreateTimeProvider(), date, includeMilliseconds, cultureInfo: CultureInfo.GetCultureInfo("en-NZ")));