diff --git a/src/Aspire.Dashboard/Components/Controls/Chart/ChartContainer.razor b/src/Aspire.Dashboard/Components/Controls/Chart/ChartContainer.razor index 9665a1226c8..a6e11297ed7 100644 --- a/src/Aspire.Dashboard/Components/Controls/Chart/ChartContainer.razor +++ b/src/Aspire.Dashboard/Components/Controls/Chart/ChartContainer.razor @@ -1,8 +1,6 @@ @namespace Aspire.Dashboard.Components -@using Aspire.Dashboard.Model @using Aspire.Dashboard.Otlp.Model -@using Aspire.Dashboard.Otlp.Model.MetricValues @using Aspire.Dashboard.Resources @using Metrics = Aspire.Dashboard.Components.Pages.Metrics @inject IStringLocalizer Loc diff --git a/src/Aspire.Dashboard/Components/Controls/Chart/ChartContainer.razor.cs b/src/Aspire.Dashboard/Components/Controls/Chart/ChartContainer.razor.cs index 3186f6af22f..5b845f8425c 100644 --- a/src/Aspire.Dashboard/Components/Controls/Chart/ChartContainer.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/Chart/ChartContainer.razor.cs @@ -13,15 +13,11 @@ namespace Aspire.Dashboard.Components; public partial class ChartContainer : ComponentBase, IAsyncDisposable { - private readonly CounterChartViewModel _viewModel = new(); - private OtlpInstrument? _instrument; private PeriodicTimer? _tickTimer; private Task? _tickTask; private IDisposable? _themeChangedSubscription; private int _renderedDimensionsCount; - private string? _previousMeterName; - private string? _previousInstrumentName; private readonly InstrumentViewModel _instrumentViewModel = new InstrumentViewModel(); [Parameter, EditorRequired] @@ -45,6 +41,9 @@ public partial class ChartContainer : ComponentBase, IAsyncDisposable [Inject] public required ThemeManager ThemeManager { get; init; } + [Inject] + public required CurrentChartViewModel ChartViewModel { get; init; } + protected override void OnInitialized() { // Update the graph every 200ms. This displays the latest data and moves time forward. @@ -113,7 +112,7 @@ private async Task UpdateInstrumentDataAsync(OtlpInstrument instrument) private bool MatchDimension(DimensionScope dimension) { - foreach (var dimensionFilter in _viewModel.DimensionFilters) + foreach (var dimensionFilter in ChartViewModel.DimensionFilters) { if (!MatchFilter(dimension.Attributes, dimensionFilter)) { @@ -156,14 +155,14 @@ protected override async Task OnParametersSetAsync() return; } - var hasInstrumentChanged = _previousMeterName != MeterName || _previousInstrumentName != InstrumentName; - _previousMeterName = MeterName; - _previousInstrumentName = InstrumentName; + var hasInstrumentChanged = ChartViewModel.PreviousMeterName != MeterName || ChartViewModel.PreviousInstrumentName != InstrumentName; + ChartViewModel.PreviousMeterName = MeterName; + ChartViewModel.PreviousInstrumentName = InstrumentName; var filters = CreateUpdatedFilters(hasInstrumentChanged); - _viewModel.DimensionFilters.Clear(); - _viewModel.DimensionFilters.AddRange(filters); + ChartViewModel.DimensionFilters.Clear(); + ChartViewModel.DimensionFilters.AddRange(filters); await UpdateInstrumentDataAsync(_instrument); } @@ -235,7 +234,7 @@ private List CreateUpdatedFilters(bool hasInstrumentCh } else { - var existing = _viewModel.DimensionFilters.SingleOrDefault(m => m.Name == item.Name); + var existing = ChartViewModel.DimensionFilters.SingleOrDefault(m => m.Name == item.Name); if (existing != null) { // Select previously selected. diff --git a/src/Aspire.Dashboard/Components/Controls/Chart/ChartFilters.razor b/src/Aspire.Dashboard/Components/Controls/Chart/ChartFilters.razor index 316b259be69..6b9f7af083a 100644 --- a/src/Aspire.Dashboard/Components/Controls/Chart/ChartFilters.razor +++ b/src/Aspire.Dashboard/Components/Controls/Chart/ChartFilters.razor @@ -6,11 +6,11 @@ @inject IStringLocalizer Loc
- @if (ViewModel.DimensionFilters.Count > 0) + @if (ChartViewModel.DimensionFilters.Count > 0) {
@Loc[nameof(ControlsStrings.ChartContainerFiltersHeader)]
- + v.Name)))"> @@ -79,7 +79,7 @@
@@ -90,9 +90,6 @@ [Parameter, EditorRequired] public required OtlpInstrument Instrument { get; set; } - [Parameter, EditorRequired] - public required CounterChartViewModel ViewModel { get; set; } - [Parameter, EditorRequired] public required InstrumentViewModel InstrumentViewModel { get; set; } } diff --git a/src/Aspire.Dashboard/Components/Controls/Chart/ChartFilters.razor.cs b/src/Aspire.Dashboard/Components/Controls/Chart/ChartFilters.razor.cs index bb59ea4ca6b..aee4604044f 100644 --- a/src/Aspire.Dashboard/Components/Controls/Chart/ChartFilters.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/Chart/ChartFilters.razor.cs @@ -1,23 +1,27 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Dashboard.Model; +using Microsoft.AspNetCore.Components; + namespace Aspire.Dashboard.Components; public partial class ChartFilters { - private bool _showCount; + [Inject] + public required CurrentChartViewModel ChartViewModel { get; init; } protected override void OnInitialized() { InstrumentViewModel.DataUpdateSubscriptions.Add(() => { - _showCount = InstrumentViewModel.ShowCount; + ChartViewModel.ShowCounts = InstrumentViewModel.ShowCount; return Task.CompletedTask; }); } private void ShowCountChanged() { - InstrumentViewModel.ShowCount = _showCount; + InstrumentViewModel.ShowCount = ChartViewModel.ShowCounts; } } diff --git a/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor b/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor index 4469a698581..d9586215072 100644 --- a/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor +++ b/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor @@ -103,7 +103,7 @@ diff --git a/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor.cs b/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor.cs index 69d69db1c35..b4c416e9a3a 100644 --- a/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor.cs @@ -4,8 +4,8 @@ using System.Diagnostics; using System.Globalization; using Aspire.Dashboard.Components.Controls.Chart; -using Aspire.Dashboard.Components.Dialogs; using Aspire.Dashboard.Model; +using Aspire.Dashboard.Components.Dialogs; using Aspire.Dashboard.Otlp.Model; using Aspire.Dashboard.Resources; using Aspire.Dashboard.Utils; @@ -24,7 +24,6 @@ public partial class MetricTable : ChartBase private OtlpInstrument? _instrument; private bool _showCount; - private bool _onlyShowValueChanges = true; private DateTimeOffset? _lastUpdate; private readonly CancellationTokenSource _waitTaskCancellationTokenSource = new(); @@ -34,6 +33,9 @@ public partial class MetricTable : ChartBase [Inject] public required IJSRuntime JS { get; init; } + [Inject] + public required CurrentChartViewModel ChartViewModel { get; init; } + [Inject] public required IDialogService DialogService { get; init; } @@ -139,7 +141,7 @@ private SortedList UpdateMetrics(out ISet DoubleEquals(diff, 0))) + if (ChartViewModel.OnlyShowValueChangesInTable && valueDiffs.All(diff => DoubleEquals(diff, 0))) { continue; } @@ -174,7 +176,7 @@ MetricViewBase CreateHistogramMetricView() continue; } - if (_onlyShowValueChanges && DoubleEquals(valueDiff, 0d)) + if (ChartViewModel.OnlyShowValueChangesInTable && DoubleEquals(valueDiff, 0d)) { continue; } diff --git a/src/Aspire.Dashboard/Components/Controls/DetailView.razor b/src/Aspire.Dashboard/Components/Controls/DetailView.razor new file mode 100644 index 00000000000..7e1234949da --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/DetailView.razor @@ -0,0 +1,56 @@ +@using Aspire.Dashboard.Components.Resize +@using Aspire.Dashboard.Resources +@inject IStringLocalizer Loc + +
+
+ @if (DetailsTitle is not null) + { +
@DetailsTitle
+ } + else if (DetailsTitleTemplate is not null) + { +
@DetailsTitleTemplate
+ } +
+ @if (ViewportInformation.IsDesktop) + { + + } + + +
+
+ @Details +
+ +@code { + private readonly Icon _splitHorizontalIcon = new Icons.Regular.Size16.SplitHorizontal(); + private readonly Icon _splitVerticalIcon = new Icons.Regular.Size16.SplitVertical(); + + [Parameter] + public string? DetailsTitle { get; set; } + + [Parameter] + public RenderFragment? Details { get; set; } + + [Parameter] + public RenderFragment? DetailsTitleTemplate { get; set; } + + [Parameter] + public required Func HandleToggleOrientation { get; set; } + + [Parameter] + public required Func HandleDismissAsync { get; set; } + + [Parameter] + public required Orientation Orientation { get; set; } + + [CascadingParameter] + public required ViewportInformation ViewportInformation { get; set; } +} diff --git a/src/Aspire.Dashboard/Components/Controls/LogLevelSelect.razor b/src/Aspire.Dashboard/Components/Controls/LogLevelSelect.razor new file mode 100644 index 00000000000..947560658ee --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/LogLevelSelect.razor @@ -0,0 +1,30 @@ +@using Aspire.Dashboard.Model.Otlp +@inject IStringLocalizer Loc + + + +@code { + [Parameter] + public bool IncludeLabel { get; set; } + + [Parameter, EditorRequired] + public required List> LogLevels { get; set; } + + [Parameter, EditorRequired] + public required SelectViewModel LogLevel { get; set; } + + [Parameter] + public EventCallback> LogLevelChanged { get; set; } + + [Parameter, EditorRequired] + public required Func HandleSelectedLogLevelChangedAsync { get; set; } +} diff --git a/src/Aspire.Dashboard/Components/Controls/LogLevelSelect.razor.cs b/src/Aspire.Dashboard/Components/Controls/LogLevelSelect.razor.cs new file mode 100644 index 00000000000..4c6fc3941ee --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/LogLevelSelect.razor.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Components; + +namespace Aspire.Dashboard.Components.Controls; + +public partial class LogLevelSelect : ComponentBase +{ + private async Task HandleSelectedLogLevelChangedInternalAsync() + { + await LogLevelChanged.InvokeAsync(LogLevel); + await HandleSelectedLogLevelChangedAsync(); + } +} + diff --git a/src/Aspire.Dashboard/Components/Controls/LogViewer.razor b/src/Aspire.Dashboard/Components/Controls/LogViewer.razor index 1624be86b72..72c6743a9f4 100644 --- a/src/Aspire.Dashboard/Components/Controls/LogViewer.razor +++ b/src/Aspire.Dashboard/Components/Controls/LogViewer.razor @@ -8,7 +8,7 @@
- +
diff --git a/src/Aspire.Dashboard/Components/Controls/LogViewer.razor.cs b/src/Aspire.Dashboard/Components/Controls/LogViewer.razor.cs index b90c34304f5..fe70efc762b 100644 --- a/src/Aspire.Dashboard/Components/Controls/LogViewer.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/LogViewer.razor.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using Aspire.Dashboard.Components.Resize; using Aspire.Dashboard.ConsoleLogs; using Aspire.Dashboard.Extensions; using Aspire.Dashboard.Model; @@ -23,6 +24,12 @@ public sealed partial class LogViewer [Inject] public required BrowserTimeProvider TimeProvider { get; init; } + [Inject] + public required LogViewerViewModel ViewModel { get; init; } + + [Inject] + public required DimensionManager DimensionManager { get; set; } + protected override async Task OnAfterRenderAsync(bool firstRender) { if (_applicationChanged) @@ -33,13 +40,22 @@ protected override async Task OnAfterRenderAsync(bool firstRender) if (firstRender) { await JS.InvokeVoidAsync("initializeContinuousScroll"); + DimensionManager.OnBrowserDimensionsChanged += OnBrowserResize; } } - private readonly LogEntries _logEntries = new(); + private void OnBrowserResize(object? o, EventArgs args) + { + InvokeAsync(async () => + { + await JS.InvokeVoidAsync("resetContinuousScrollPosition"); + await JS.InvokeVoidAsync("initializeContinuousScroll"); + }); + } - internal async Task SetLogSourceAsync(IAsyncEnumerable> batches, bool convertTimestampsFromUtc) + internal async Task SetLogSourceAsync(string resourceName, IAsyncEnumerable> batches, bool convertTimestampsFromUtc) { + ViewModel.ResourceName = resourceName; _convertTimestampsFromUtc = convertTimestampsFromUtc; var cancellationToken = await _cancellationSeries.NextAsync(); @@ -57,12 +73,12 @@ internal async Task SetLogSourceAsync(IAsyncEnumerable ControlsStringsLoc + + + + @foreach (var (resourceType, _) in AllResourceTypes) + { + var isChecked = VisibleResourceTypes.ContainsKey(resourceType); + + } + +@code { + [Parameter, EditorRequired] + public required ConcurrentDictionary AllResourceTypes { get; set; } + + [Parameter, EditorRequired] + public required Func AreAllTypesVisible { get; set; } + + [Parameter, EditorRequired] + public required ConcurrentDictionary VisibleResourceTypes { get; set; } + + [Parameter, EditorRequired] + public required Action OnAllResourceTypesCheckedChanged { get; set; } + + [Parameter, EditorRequired] + public required Func OnResourceTypeVisibilityChangedAsync { get; set; } +} diff --git a/src/Aspire.Dashboard/Components/Controls/SummaryDetailsView.razor b/src/Aspire.Dashboard/Components/Controls/SummaryDetailsView.razor index 8af570e5013..37e4449af0f 100644 --- a/src/Aspire.Dashboard/Components/Controls/SummaryDetailsView.razor +++ b/src/Aspire.Dashboard/Components/Controls/SummaryDetailsView.razor @@ -3,40 +3,50 @@ @typeparam T
- - + @if (!ViewportInformation.IsDesktop) + { + @if (_internalShowDetails) + { + + } + else + {
@Summary
-
- -
-
- @if (DetailsTitle is not null) - { -
@DetailsTitle
- } - else if (DetailsTitleTemplate is not null) - { -
@DetailsTitleTemplate
- } -
- - -
-
- @Details -
-
-
+ } + } + else + { + + +
+ @Summary +
+
+ + @if (_internalShowDetails) + { + + } + +
+ }
diff --git a/src/Aspire.Dashboard/Components/Controls/SummaryDetailsView.razor.cs b/src/Aspire.Dashboard/Components/Controls/SummaryDetailsView.razor.cs index 818253273a6..b8bf7b144ab 100644 --- a/src/Aspire.Dashboard/Components/Controls/SummaryDetailsView.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/SummaryDetailsView.razor.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using Aspire.Dashboard.Components.Resize; using Aspire.Dashboard.Model; using Aspire.Dashboard.Utils; using Microsoft.AspNetCore.Components; @@ -17,7 +18,7 @@ public partial class SummaryDetailsView : IGlobalKeydownListener, IDisposable public RenderFragment? Summary { get; set; } [Parameter] - public RenderFragment? Details { get; set; } + public RenderFragment? Details { get; set; } [Parameter] public bool ShowDetails { get; set; } @@ -49,7 +50,7 @@ public partial class SummaryDetailsView : IGlobalKeydownListener, IDisposable public string? ViewKey { get; set; } [Parameter] - public RenderFragment? DetailsTitleTemplate { get; set; } + public RenderFragment? DetailsTitleTemplate { get; set; } [Inject] public required ProtectedLocalStorage ProtectedLocalStore { get; init; } @@ -63,8 +64,8 @@ public partial class SummaryDetailsView : IGlobalKeydownListener, IDisposable [Inject] public required ShortcutManager ShortcutManager { get; init; } - private readonly Icon _splitHorizontalIcon = new Icons.Regular.Size16.SplitHorizontal(); - private readonly Icon _splitVerticalIcon = new Icons.Regular.Size16.SplitVertical(); + [CascadingParameter] + public required ViewportInformation ViewportInformation { get; set; } private string _panel1Size { get; set; } = "1fr"; private string _panel2Size { get; set; } = "1fr"; @@ -107,6 +108,7 @@ protected override async Task OnParametersSetAsync() // This is required because we only want to show details after resolving size and orientation // to avoid a flash of content in the wrong location. _internalShowDetails = ShowDetails; + SetPanelToFullScreenOnMobile(); } private async Task HandleDismissAsync() @@ -185,6 +187,15 @@ private void SetPanelSizes(float panel1Fraction) _panel2Size = string.Create(CultureInfo.InvariantCulture, $"{(1 - panel1Fraction):F3}fr"); } + private void SetPanelToFullScreenOnMobile() + { + if (!ViewportInformation.IsDesktop) + { + // panel 1 will have a height of 0, so its fraction of 1 is also 0 + SetPanelSizes(panel1Fraction: 0); + } + } + public IReadOnlySet SubscribedShortcuts { get; } = new HashSet { AspireKeyboardShortcut.ToggleOrientation, diff --git a/src/Aspire.Dashboard/Components/Controls/SummaryDetailsView.razor.css b/src/Aspire.Dashboard/Components/Controls/SummaryDetailsView.razor.css index b8dde60d489..d8eb6ad1f29 100644 --- a/src/Aspire.Dashboard/Components/Controls/SummaryDetailsView.razor.css +++ b/src/Aspire.Dashboard/Components/Controls/SummaryDetailsView.razor.css @@ -1,5 +1,11 @@ +@media (min-width: 768px) { + .summary-details-container { + overflow: auto; + } +} + .summary-details-container { - overflow: auto; + height: 100%; } ::deep split-panels { @@ -10,12 +16,10 @@ ::deep .summary-container { height: 100%; min-width: 100%; - overflow: auto; } ::deep .details-container { height: 100%; - overflow: auto; display: grid; grid-template-rows: auto 1fr; grid-template-areas: @@ -26,7 +30,6 @@ ::deep .details-container > header { height: auto; grid-row-start: 1; - background-color: var(--neutral-layer-4); color: var(--neutral-foreground-rest); padding: calc(var(--design-unit) * 2px) calc(var(--design-unit) * 3px); display: grid; @@ -35,6 +38,15 @@ align-items: center; } +/* At lower widths we want to use the same background color as the rest of the page, as the header + is our page header too +*/ +@media (min-width: 768px) { + ::deep .details-container > header { + background-color: var(--neutral-layer-4); + } +} + ::deep .details-container > header fluent-button[appearance=stealth]:not(:hover)::part(control) { background-color: var(--neutral-layer-4); } diff --git a/src/Aspire.Dashboard/Components/Layout/AspirePageContentLayout.razor b/src/Aspire.Dashboard/Components/Layout/AspirePageContentLayout.razor new file mode 100644 index 00000000000..c3b1c45127e --- /dev/null +++ b/src/Aspire.Dashboard/Components/Layout/AspirePageContentLayout.razor @@ -0,0 +1,77 @@ +@using Aspire.Dashboard.Resources + +@inject IStringLocalizer LayoutLoc +@inject IStringLocalizer ControlsStringsLoc + +
+ @if (ViewportInformation.IsDesktop) + { +
+ @if (AddNewlineOnToolbar) + { + @PageTitleSection + + + @ToolbarSection + + } + else + { + + @PageTitleSection + @ToolbarSection + + } +
+ +
@MainSection
+ + @if (FooterSection is not null && ShouldShowFooter) + { +
@FooterSection
+ } + } + else if (IsSummaryDetailsViewOpen) + { +
@MainSection
+ } + else + { +
+ + @PageTitleSection + + @if (!AddNewlineOnToolbar) + { + @MobilePageTitleToolbarSection + } + + + @if (AddNewlineOnToolbar) + { +
@MobilePageTitleToolbarSection
+ } +
+ +
@MainSection
+ + if ((FooterSection is not null && ShouldShowFooter) || ToolbarSection is not null) + { +
+ @if (FooterSection is not null && ShouldShowFooter) + { + @FooterSection + } + + @if (ToolbarSection is not null) + { + + @(MobileToolbarButtonText ?? LayoutLoc[nameof(Layout.PageLayoutViewFilters)]) + + } +
+ } + } +
diff --git a/src/Aspire.Dashboard/Components/Layout/AspirePageContentLayout.razor.cs b/src/Aspire.Dashboard/Components/Layout/AspirePageContentLayout.razor.cs new file mode 100644 index 00000000000..c5ff2cbd124 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Layout/AspirePageContentLayout.razor.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Dashboard.Components.Resize; +using Aspire.Dashboard.Resources; +using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components; + +namespace Aspire.Dashboard.Components.Layout; + +public partial class AspirePageContentLayout : ComponentBase +{ + [CascadingParameter] + public required ViewportInformation ViewportInformation { get; init; } + + [Parameter] + public required RenderFragment PageTitleSection { get; set; } + + [Parameter] + public RenderFragment? MobilePageTitleToolbarSection { get; set; } + + [Parameter] + public RenderFragment? ToolbarSection { get; set; } + + [Parameter] + public bool AddNewlineOnToolbar { get; set; } + + [Parameter] + public RenderFragment? MainSection { get; set; } + + [Parameter] + public RenderFragment? FooterSection { get; set; } + + [Parameter] + public bool ShouldShowFooter { get; set; } = true; + + [Parameter] + public string? MobileToolbarButtonText { get; set; } + + [Parameter] + public string? HeaderStyle { get; set; } + + [Parameter] + public string? MainContentStyle { get; set; } + + [Parameter] + public bool IsSummaryDetailsViewOpen { get; set; } + + [Inject] + public required IDialogService DialogService { get; init; } + + [Inject] + public required NavigationManager NavigationManager { get; init; } + + private IDialogReference? _toolbarPanel; + + public bool IsToolbarPanelOpen => _toolbarPanel is not null; + + public Dictionary> DialogCloseListeners { get; } = new(); + + protected override async Task OnParametersSetAsync() + { + if (ViewportInformation.IsDesktop && IsToolbarPanelOpen) + { + await CloseMobileToolbarAsync(); + } + } + + private string GetMobileMainStyle() + { + var style = "grid-area: main;" + MainContentStyle; + if (!ViewportInformation.IsUltraLowHeight) + { + style += "overflow: auto;"; + } + + return style; + } + + private async Task OpenMobileToolbarAsync() + { + _toolbarPanel = await DialogService.ShowPanelAsync( + new MobileToolbar( + ToolbarSection!, + MobileToolbarButtonText ?? LayoutLoc[nameof(Resources.Layout.PageLayoutViewFilters)]), + new DialogParameters + { + Alignment = HorizontalAlignment.Center, + Title = MobileToolbarButtonText ?? ControlsStringsLoc[nameof(ControlsStrings.ChartContainerFiltersHeader)], + Width = "100%", + Height = "90%", + Modal = false, + PrimaryAction = null, + SecondaryAction = null, + OnDialogClosing = EventCallback.Factory.Create(this, InvokeListeners) + }); + } + + public async Task CloseMobileToolbarAsync() + { + if (_toolbarPanel is not null) + { + await _toolbarPanel.CloseAsync(); + // CloseAsync doesn't invoke OnDialogClosing, so we need to call InvokeListeners ourselves + await InvokeListeners(); + + _toolbarPanel = null; + } + } + + private async Task InvokeListeners() + { + foreach (var dialogCloseListener in DialogCloseListeners.Values) + { + await dialogCloseListener.Invoke(); + } + + DialogCloseListeners.Clear(); + } + + public record MobileToolbar(RenderFragment ToolbarSection, string MobileToolbarButtonText); +} + diff --git a/src/Aspire.Dashboard/Components/Layout/AspirePageContentLayout.razor.css b/src/Aspire.Dashboard/Components/Layout/AspirePageContentLayout.razor.css new file mode 100644 index 00000000000..685ebd8066f --- /dev/null +++ b/src/Aspire.Dashboard/Components/Layout/AspirePageContentLayout.razor.css @@ -0,0 +1,20 @@ +::deep.content-layout { + height: 100%; + width: 100%; + display: grid; + + grid-template-areas: + "page-header" + "main" + "footer"; + + grid-template-rows: auto 1fr auto; +} + +::deep .title-toolbar-inline { + padding-left: 0px; +} + +::deep .title-toolbar-inline > h1 { + margin-left: 0; +} diff --git a/src/Aspire.Dashboard/Components/Layout/NavMenu.razor b/src/Aspire.Dashboard/Components/Layout/DesktopNavMenu.razor similarity index 100% rename from src/Aspire.Dashboard/Components/Layout/NavMenu.razor rename to src/Aspire.Dashboard/Components/Layout/DesktopNavMenu.razor diff --git a/src/Aspire.Dashboard/Components/Layout/NavMenu.razor.cs b/src/Aspire.Dashboard/Components/Layout/DesktopNavMenu.razor.cs similarity index 69% rename from src/Aspire.Dashboard/Components/Layout/NavMenu.razor.cs rename to src/Aspire.Dashboard/Components/Layout/DesktopNavMenu.razor.cs index 7501978f5c4..73de261adea 100644 --- a/src/Aspire.Dashboard/Components/Layout/NavMenu.razor.cs +++ b/src/Aspire.Dashboard/Components/Layout/DesktopNavMenu.razor.cs @@ -6,25 +6,25 @@ namespace Aspire.Dashboard.Components.Layout; -public partial class NavMenu : ComponentBase +public partial class DesktopNavMenu : ComponentBase { - private static Icon ResourcesIcon(bool active = false) => + internal static Icon ResourcesIcon(bool active = false) => active ? new Icons.Filled.Size24.AppFolder() : new Icons.Regular.Size24.AppFolder(); - private static Icon ConsoleLogsIcon(bool active = false) => + internal static Icon ConsoleLogsIcon(bool active = false) => active ? new Icons.Filled.Size24.SlideText() : new Icons.Regular.Size24.SlideText(); - private static Icon StructuredLogsIcon(bool active = false) => + internal static Icon StructuredLogsIcon(bool active = false) => active ? new Icons.Filled.Size24.SlideTextSparkle() : new Icons.Regular.Size24.SlideTextSparkle(); - private static Icon TracesIcon(bool active = false) => + internal static Icon TracesIcon(bool active = false) => active ? new Icons.Filled.Size24.GanttChart() : new Icons.Regular.Size24.GanttChart(); - private static Icon MetricsIcon(bool active = false) => + internal static Icon MetricsIcon(bool active = false) => active ? new Icons.Filled.Size24.ChartMultiple() : new Icons.Regular.Size24.ChartMultiple(); } diff --git a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor index 30f6a5873e5..4b498ac830a 100644 --- a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor +++ b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor @@ -1,48 +1,76 @@ -@using Aspire.Dashboard.Components.CustomIcons; +@using Aspire.Dashboard.Components.CustomIcons @using Aspire.Dashboard.Resources -@using Aspire.Dashboard.Utils @inherits LayoutComponentBase -
+
- - -
- - - - - - - - - - -
- + @if (ViewportInformation.IsDesktop) + { + + +
+ + + + + + + + + + +
+ + + } + else + { + + +
+ + + +
+ + + } +
- +
- - + + @Body - - + +
@Loc[nameof(Layout.MainLayoutUnhandledErrorMessage)] @Loc[nameof(Layout.MainLayoutUnhandledErrorReload)] diff --git a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs index 34049f43eb7..3fddc574db0 100644 --- a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs +++ b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Dashboard.Components.Dialogs; +using Aspire.Dashboard.Components.Resize; using Aspire.Dashboard.Configuration; using Aspire.Dashboard.Model; using Aspire.Dashboard.Utils; @@ -15,6 +16,8 @@ namespace Aspire.Dashboard.Components.Layout; public partial class MainLayout : IGlobalKeydownListener, IAsyncDisposable { + private bool _isNavMenuOpen; + private IDisposable? _themeChangedSubscription; private IDisposable? _locationChangingRegistration; private IJSObjectReference? _jsModule; @@ -38,6 +41,9 @@ public partial class MainLayout : IGlobalKeydownListener, IAsyncDisposable [Inject] public required IStringLocalizer Loc { get; init; } + [Inject] + public required IStringLocalizer DialogsLoc { get; init; } + [Inject] public required IDialogService DialogService { get; init; } @@ -56,6 +62,9 @@ public partial class MainLayout : IGlobalKeydownListener, IAsyncDisposable [Inject] public required IOptionsMonitor Options { get; init; } + [CascadingParameter] + public required ViewportInformation ViewportInformation { get; set; } + protected override async Task OnInitializedAsync() { // Theme change can be triggered from the settings dialog. This logic applies the new theme to the browser window. @@ -120,6 +129,15 @@ protected override async Task OnAfterRenderAsync(bool firstRender) } } + protected override void OnParametersSet() + { + if (ViewportInformation.IsDesktop && _isNavMenuOpen) + { + _isNavMenuOpen = false; + CloseMobileNavMenu(); + } + } + private async Task LaunchHelpAsync() { DialogParameters parameters = new() @@ -224,6 +242,12 @@ public async Task OnPageKeyDownAsync(AspireKeyboardShortcut shortcut) } } + private void CloseMobileNavMenu() + { + _isNavMenuOpen = false; + StateHasChanged(); + } + public async ValueTask DisposeAsync() { _shortcutManagerReference?.Dispose(); diff --git a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css index 0cc0d10f4de..5116923bfa4 100644 --- a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css +++ b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css @@ -1,107 +1,235 @@ -#blazor-error-ui { - background: var(--highlight-bg); - bottom: 0; - box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); - display: none; - left: 0; - padding: 0.6rem 1.25rem 0.7rem 1.25rem; - position: fixed; - width: 100%; - z-index: 1000; - color: var(--error-ui-foreground-color); -} - -#blazor-error-ui a { - color: var(--error-ui-accent-foreground-color); -} - -#blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; -} - -#blazor-error-ui .reload { - color: var(--error-ui-accent-foreground-color); -} - -::deep.layout { - height: 100vh; - width: 100vw; - display: grid; - grid-template-columns: auto 1fr; - grid-template-rows: auto auto 1fr; - grid-template-areas: +@media (min-width: 768px) { + #blazor-error-ui { + background: var(--highlight-bg); + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; + color: var(--error-ui-foreground-color); + } + + #blazor-error-ui a { + color: var(--error-ui-accent-foreground-color); + } + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + + #blazor-error-ui .reload { + color: var(--error-ui-accent-foreground-color); + } + + ::deep.layout { + height: 100vh; + width: 100vw; + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: auto auto 1fr; + grid-template-areas: "icon head" "nav messagebar" "nav main"; - background-color: var(--fill-color); - color: var(--neutral-foreground-rest); -} - -::deep.layout > .aspire-icon { - grid-area: icon; - display: flex; - align-items: center; - justify-content: center; - background-color: var(--neutral-layer-4); -} - -::deep.layout > header { - grid-area: head; -} - -::deep.layout > .nav-menu-container { - grid-area: nav; - overflow-y: auto; - background: var(--neutral-layer-4); -} - -::deep.layout > .body-content { - grid-area: main; - overflow-x: auto; /* allow horizontal scrolling */ - border-left: 1px solid var(--neutral-stroke-rest); -} - -::deep.layout > .messagebar-container { - grid-area: messagebar; - border-top: 1px solid var(--neutral-stroke-rest); - border-left: 1px solid var(--neutral-stroke-rest); -} - -::deep .header-right { - margin-left: auto; -} - -::deep.layout > header > .header-gutters > fluent-button[appearance=stealth]:not(:hover)::part(control), -::deep.layout > header > .header-gutters > fluent-anchor[appearance=stealth]:not(:hover)::part(control), -::deep.layout > header > .header-gutters > fluent-anchor[appearance=stealth].logo::part(control), -::deep.layout > .aspire-icon fluent-anchor[appearance=stealth].logo::part(control) { - background-color: var(--neutral-layer-4); -} - -::deep.layout > header { - background-color: var(--neutral-layer-4); - margin-bottom: 0; -} - -::deep.layout > header > .header-gutters > fluent-anchor { - font-size: var(--type-ramp-plus-2-font-size); -} - -::deep .aspire-icon fluent-anchor.logo::part(control) { - padding: 0; - border: none; -} - -::deep .aspire-icon fluent-anchor.logo, -::deep .aspire-icon fluent-anchor.logo::part(control), -::deep .aspire-icon fluent-anchor.logo::part(content) { - height: 24px; - width: 24px; -} - -::deep.layout > header > .header-gutters { - margin-left: 0; + background-color: var(--fill-color); + color: var(--neutral-foreground-rest); + } + + ::deep.layout > .aspire-icon { + grid-area: icon; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--neutral-layer-4); + } + + ::deep.layout > header { + grid-area: head; + } + + ::deep.layout > .nav-menu-container { + grid-area: nav; + overflow-y: auto; + background: var(--neutral-layer-4); + } + + ::deep.layout > .body-content { + grid-area: main; + overflow-x: auto; /* allow horizontal scrolling */ + overflow: auto; + border-left: 1px solid var(--neutral-stroke-rest); + } + + ::deep.layout > .messagebar-container { + grid-area: messagebar; + border-top: 1px solid var(--neutral-stroke-rest); + border-left: 1px solid var(--neutral-stroke-rest); + } + + ::deep .header-right { + margin-left: auto; + } + + ::deep.layout > header > .header-gutters > fluent-button[appearance=stealth]:not(:hover)::part(control), + ::deep.layout > header > .header-gutters > fluent-anchor[appearance=stealth]:not(:hover)::part(control), + ::deep.layout > header > .header-gutters > fluent-anchor[appearance=stealth].logo::part(control), + ::deep.layout > .aspire-icon fluent-anchor[appearance=stealth].logo::part(control) { + background-color: var(--neutral-layer-4); + } + + ::deep.layout > header { + background-color: var(--neutral-layer-4); + margin-bottom: 0; + } + + ::deep.layout > header > .header-gutters > fluent-anchor { + font-size: var(--type-ramp-plus-2-font-size); + } + + ::deep .aspire-icon fluent-anchor.logo::part(control) { + padding: 0; + border: none; + } + + ::deep .aspire-icon fluent-anchor.logo, + ::deep .aspire-icon fluent-anchor.logo::part(control), + ::deep .aspire-icon fluent-anchor.logo::part(content) { + height: 24px; + width: 24px; + } + + ::deep.layout > header > .header-gutters { + margin-left: 0; + } +} + +@media (max-width: 768px) { + #blazor-error-ui { + background: var(--highlight-bg); + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; + color: var(--error-ui-foreground-color); + } + + #blazor-error-ui a { + color: var(--error-ui-accent-foreground-color); + } + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + + #blazor-error-ui .reload { + color: var(--error-ui-accent-foreground-color); + } + + ::deep.layout { + height: 100vh; + width: 100vw; + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: auto auto auto 1fr; + grid-template-areas: + "icon head" + "nav-menu nav-menu" + "messagebar messagebar" + "main main"; + background-color: var(--fill-color); + color: var(--neutral-foreground-rest); + } + + ::deep.layout > .aspire-icon { + grid-area: icon; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--neutral-layer-4); + } + + ::deep.layout > header { + grid-area: head; + } + + ::deep.layout > .nav-menu-container { + grid-area: nav; + overflow-y: auto; + background: var(--neutral-layer-4); + } + + ::deep.layout > .body-content { + grid-area: main; + overflow-x: auto; /* allow horizontal scrolling */ + overflow: auto; + border-left: 1px solid var(--neutral-stroke-rest); + } + + ::deep.layout > .messagebar-container { + grid-area: messagebar; + border-top: 1px solid var(--neutral-stroke-rest); + border-left: 1px solid var(--neutral-stroke-rest); + } + + ::deep .header-right { + margin-left: auto; + } + + ::deep.layout > header > .header-gutters > fluent-button[appearance=stealth]:not(:hover)::part(control), + ::deep.layout > header > .header-gutters > fluent-anchor[appearance=stealth]:not(:hover)::part(control), + ::deep.layout > header > .header-gutters > fluent-anchor[appearance=stealth].logo::part(control), + ::deep.layout > .aspire-icon fluent-anchor[appearance=stealth].logo::part(control) { + background-color: var(--neutral-layer-4); + } + + ::deep.layout > .aspire-icon { + padding-left: 15px; + background-color: var(--neutral-layer-4); + } + + ::deep.layout > .aspire-icon fluent-anchor { + margin-left: 5px; + } + + ::deep.layout > .aspire-icon .navigation-button { + border: 1px solid #a8aeb3; + } + + ::deep.layout > header { + background-color: var(--neutral-layer-4); + margin-bottom: 0; + } + + ::deep.layout > header > .header-gutters > fluent-anchor { + font-size: var(--type-ramp-plus-2-font-size); + } + + ::deep .aspire-icon fluent-anchor.logo::part(control) { + padding: 0; + border: none; + } + + ::deep .aspire-icon fluent-anchor.logo, + ::deep .aspire-icon fluent-anchor.logo::part(control), + ::deep .aspire-icon fluent-anchor.logo::part(content) { + height: 24px; + width: 24px; + } + + ::deep.layout > header > .header-gutters { + margin-left: 0; + } } diff --git a/src/Aspire.Dashboard/Components/Layout/MobileNavMenu.razor b/src/Aspire.Dashboard/Components/Layout/MobileNavMenu.razor new file mode 100644 index 00000000000..04a4a8447ee --- /dev/null +++ b/src/Aspire.Dashboard/Components/Layout/MobileNavMenu.razor @@ -0,0 +1,40 @@ + + @foreach (var item in GetMobileNavMenuEntries()) + { + + @item.Text + + @if (item.Icon is { } icon) + { + var isActive = item.LinkMatchRegex is not null && item.LinkMatchRegex.IsMatch($"/{NavigationManager.ToBaseRelativePath(NavigationManager.Uri)}"); + + + @if (isActive) + { + + } + else + { + + } + + } + + + + } + + +@code { + [Parameter, EditorRequired] + public required bool IsNavMenuOpen { get; set; } + + [Parameter, EditorRequired] + public required Action CloseNavMenu { get; set; } + + [Parameter, EditorRequired] + public required Func LaunchHelpAsync { get; set; } + + [Parameter, EditorRequired] + public required Func LaunchSettingsAsync { get; set; } +} diff --git a/src/Aspire.Dashboard/Components/Layout/MobileNavMenu.razor.cs b/src/Aspire.Dashboard/Components/Layout/MobileNavMenu.razor.cs new file mode 100644 index 00000000000..a93034900a9 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Layout/MobileNavMenu.razor.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; +using Aspire.Dashboard.Components.CustomIcons; +using Aspire.Dashboard.Model; +using Aspire.Dashboard.Utils; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; +using Microsoft.FluentUI.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace Aspire.Dashboard.Components.Layout; + +public partial class MobileNavMenu : ComponentBase +{ + [Inject] + public required NavigationManager NavigationManager { get; init; } + + [Inject] + public required IDashboardClient DashboardClient { get; init; } + + [Inject] + public required IStringLocalizer Loc { get; init; } + + [Inject] + public required IJSRuntime JS { get; init; } + + private Task NavigateToAsync(string url) + { + NavigationManager.NavigateTo(url); + return Task.CompletedTask; + } + + private IEnumerable GetMobileNavMenuEntries() + { + if (DashboardClient.IsEnabled) + { + yield return new MobileNavMenuEntry( + Loc[nameof(Resources.Layout.NavMenuResourcesTab)], + () => NavigateToAsync(DashboardUrls.ResourcesUrl()), + DesktopNavMenu.ResourcesIcon(), + LinkMatchRegex: new Regex($"^{DashboardUrls.ResourcesUrl()}$") + ); + + yield return new MobileNavMenuEntry( + Loc[nameof(Resources.Layout.NavMenuConsoleLogsTab)], + () => NavigateToAsync(DashboardUrls.ConsoleLogsUrl()), + DesktopNavMenu.ConsoleLogsIcon(), + LinkMatchRegex: GetNonIndexPageRegex(DashboardUrls.ConsoleLogsUrl()) + ); + } + + yield return new MobileNavMenuEntry( + Loc[nameof(Resources.Layout.NavMenuStructuredLogsTab)], + () => NavigateToAsync(DashboardUrls.StructuredLogsUrl()), + DesktopNavMenu.StructuredLogsIcon(), + LinkMatchRegex: GetNonIndexPageRegex(DashboardUrls.StructuredLogsUrl()) + ); + + yield return new MobileNavMenuEntry( + Loc[nameof(Resources.Layout.NavMenuTracesTab)], + () => NavigateToAsync(DashboardUrls.TracesUrl()), + DesktopNavMenu.TracesIcon(), + LinkMatchRegex: GetNonIndexPageRegex(DashboardUrls.TracesUrl()) + ); + + yield return new MobileNavMenuEntry( + Loc[nameof(Resources.Layout.NavMenuMetricsTab)], + () => NavigateToAsync(DashboardUrls.MetricsUrl()), + DesktopNavMenu.MetricsIcon(), + LinkMatchRegex: GetNonIndexPageRegex(DashboardUrls.MetricsUrl()) + ); + + yield return new MobileNavMenuEntry( + Loc[nameof(Resources.Layout.MainLayoutAspireRepoLink)], + async () => + { + await JS.InvokeVoidAsync("open", ["https://aka.ms/dotnet/aspire/repo", "_blank"]); + }, + new AspireIcons.Size24.GitHub() + ); + + yield return new MobileNavMenuEntry( + Loc[nameof(Resources.Layout.MainLayoutAspireDashboardHelpLink)], + LaunchHelpAsync, + new Icons.Regular.Size24.QuestionCircle() + ); + + yield return new MobileNavMenuEntry( + Loc[nameof(Resources.Layout.MainLayoutLaunchSettings)], + LaunchSettingsAsync, + new Icons.Regular.Size24.Settings() + ); + } + + private static Regex GetNonIndexPageRegex(string pageRelativeBasePath) + { + pageRelativeBasePath = Regex.Escape(pageRelativeBasePath); + return new Regex($"^({pageRelativeBasePath}|{pageRelativeBasePath}/.+)$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + } +} + diff --git a/src/Aspire.Dashboard/Components/Layout/MobileNavMenuEntry.cs b/src/Aspire.Dashboard/Components/Layout/MobileNavMenuEntry.cs new file mode 100644 index 00000000000..b0cf8407a68 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Layout/MobileNavMenuEntry.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; +using Microsoft.FluentUI.AspNetCore.Components; + +namespace Aspire.Dashboard.Components.Layout; + +internal record MobileNavMenuEntry(string Text, Func OnClick, Icon? Icon = null, Regex? LinkMatchRegex = null); diff --git a/src/Aspire.Dashboard/Components/Layout/ToolbarPanel.razor b/src/Aspire.Dashboard/Components/Layout/ToolbarPanel.razor new file mode 100644 index 00000000000..c6d3a4065a1 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Layout/ToolbarPanel.razor @@ -0,0 +1,18 @@ +@implements IDialogContentComponent + + + + @Content.ToolbarSection + + + + @Content.MobileToolbarButtonText + + + +@code { + [Parameter] + public required AspirePageContentLayout.MobileToolbar Content { get; set; } +} diff --git a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor index dbbc47f644e..31fcf5e342a 100644 --- a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor +++ b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor @@ -8,15 +8,42 @@ -
-

@Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsHeader)]

- - - @PageViewModel.Status - - +
+ + +

@Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsHeader)]

+
+ + + @if (ViewportInformation.IsDesktop) + { + // This takes up too much horizontal space on mobile, so show on a new line on mobile + @PageViewModel.Status + } + + + + + @if (PageViewModel.SelectedOption.Id is not null) + { + @($"{PageViewModel.SelectedOption.Name}: {PageViewModel.Status}") + } + else + { + @PageViewModel.Status + } + + + + + + +
diff --git a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs index 48d91285d70..1ec61843476 100644 --- a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs @@ -5,6 +5,8 @@ using System.Collections.Immutable; using System.Diagnostics; using Aspire.Dashboard.Components.Controls; +using Aspire.Dashboard.Components.Layout; +using Aspire.Dashboard.Components.Resize; using Aspire.Dashboard.Extensions; using Aspire.Dashboard.Model; using Aspire.Dashboard.Model.Otlp; @@ -30,6 +32,15 @@ public sealed partial class ConsoleLogs : ComponentBase, IAsyncDisposable, IPage [Inject] public required ILogger Logger { get; init; } + [Inject] + public required LogViewerViewModel LogViewerViewModel { get; init; } + + [Inject] + public required DimensionManager DimensionManager { get; init; } + + [CascadingParameter] + public required ViewportInformation ViewportInformation { get; init; } + [Parameter] public string? ResourceName { get; set; } @@ -44,6 +55,7 @@ public sealed partial class ConsoleLogs : ComponentBase, IAsyncDisposable, IPage private ResourceSelect? _resourceSelectComponent; private SelectViewModel _noSelection = null!; private LogViewer _logViewer = null!; + private AspirePageContentLayout? _contentLayout; // State public ConsoleLogsViewModel PageViewModel { get; set; } = null!; @@ -142,6 +154,11 @@ void SetSelectedResourceOption(ResourceViewModel resource) protected override async Task OnParametersSetAsync() { + if (!DimensionManager.IsResizing && PageViewModel.InitialisedSuccessfully is true && StringComparers.ResourceName.Equals(ResourceName, LogViewerViewModel.ResourceName)) + { + return; + } + Logger.LogDebug("Initializing console logs view model."); await this.InitializeViewModelAsync(); @@ -255,6 +272,7 @@ private async ValueTask LoadLogsAsync() if (subscription is not null) { var task = _logViewer.SetLogSourceAsync( + PageViewModel.SelectedResource.Name, subscription, convertTimestampsFromUtc: PageViewModel.SelectedResource.IsContainer()); @@ -284,7 +302,7 @@ private async Task HandleSelectedOptionChangedAsync() await ClearLogsAsync(); PageViewModel.SelectedResource = PageViewModel.SelectedOption?.Id?.InstanceId is null ? null : _resourceByName[PageViewModel.SelectedOption.Id.InstanceId]; - await this.AfterViewModelChangedAsync(); + await this.AfterViewModelChangedAsync(_contentLayout, isChangeInToolbar: false); } private async Task OnResourceChanged(ResourceViewModelChangeType changeType, ResourceViewModel resource) diff --git a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.css b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.css deleted file mode 100644 index eb0af27a468..00000000000 --- a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.css +++ /dev/null @@ -1,24 +0,0 @@ -::deep.resource-logs-layout { - display: grid; - grid-template-rows: auto auto 1fr; - height: 100%; - width: 100%; - grid-template-areas: - "head" - "toolbar" - "main"; - gap: calc(var(--design-unit) * 2px); -} - -::deep.resource-logs-layout > h1 { - grid-area: head; -} - -::deep.resource-logs-layout > fluent-toolbar { - grid-area: toolbar; - padding: var(--layout-toolbar-padding); -} - -::deep.resource-logs-layout > .log-overflow { - grid-area: main; -} diff --git a/src/Aspire.Dashboard/Components/Pages/IPageWithSessionAndUrlState.cs b/src/Aspire.Dashboard/Components/Pages/IPageWithSessionAndUrlState.cs index f17853cc880..b9a33267a89 100644 --- a/src/Aspire.Dashboard/Components/Pages/IPageWithSessionAndUrlState.cs +++ b/src/Aspire.Dashboard/Components/Pages/IPageWithSessionAndUrlState.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Dashboard.Components.Layout; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; @@ -56,13 +57,27 @@ public static class PageExtensions /// Called after a change in the view model that will affect the url associated with new page state /// to navigate to the new url and save new state in localstorage. /// - public static async Task AfterViewModelChangedAsync(this IPageWithSessionAndUrlState page) where TSerializableViewModel : class + public static async Task AfterViewModelChangedAsync(this IPageWithSessionAndUrlState page, AspirePageContentLayout? layout, bool isChangeInToolbar) where TSerializableViewModel : class { - var serializableViewModel = page.ConvertViewModelToSerializable(); - var pathWithParameters = page.GetUrlFromSerializableViewModel(serializableViewModel).ToString(); + // if the mobile filter dialog is open, we want to wait until the dialog is closed to apply all changes + // we should only apply the last invocation, as TViewModel will be up-to-date + if (layout is not null && !layout.ViewportInformation.IsDesktop && isChangeInToolbar) + { + layout.DialogCloseListeners[nameof(AfterViewModelChangedAsync)] = SetStateAndNavigateAsync; + return; + } + + await SetStateAndNavigateAsync(); + return; - page.NavigationManager.NavigateTo(pathWithParameters); - await page.SessionStorage.SetAsync(page.SessionStorageKey, serializableViewModel).ConfigureAwait(false); + async Task SetStateAndNavigateAsync() + { + var serializableViewModel = page.ConvertViewModelToSerializable(); + var pathWithParameters = page.GetUrlFromSerializableViewModel(serializableViewModel); + + page.NavigationManager.NavigateTo(pathWithParameters); + await page.SessionStorage.SetAsync(page.SessionStorageKey, serializableViewModel).ConfigureAwait(false); + } } public static async Task InitializeViewModelAsync(this IPageWithSessionAndUrlState page) where TSerializableViewModel : class diff --git a/src/Aspire.Dashboard/Components/Pages/Metrics.razor b/src/Aspire.Dashboard/Components/Pages/Metrics.razor index 76c8d30debc..2ebc3a250fc 100644 --- a/src/Aspire.Dashboard/Components/Pages/Metrics.razor +++ b/src/Aspire.Dashboard/Components/Pages/Metrics.razor @@ -12,87 +12,96 @@ -
-

@Loc[nameof(Dashboard.Resources.Metrics.MetricsHeader)]

- - - - - -
- @if (PageViewModel.Instruments?.Count > 0) - { - - - - - @foreach (var meterGroup in PageViewModel.Instruments.GroupBy(i => i.Parent).OrderBy(g => g.Key.MeterName)) - { - - @foreach (var instrument in meterGroup.OrderBy(i => i.Name)) +
+ + +

@Loc[nameof(Dashboard.Resources.Metrics.MetricsHeader)]

+
+ + + + + + +
+ @if (PageViewModel.Instruments?.Count > 0) + { + + + + + @foreach (var meterGroup in PageViewModel.Instruments.GroupBy(i => i.Parent).OrderBy(g => g.Key.MeterName)) { - + + @foreach (var instrument in meterGroup.OrderBy(i => i.Name)) + { + + } + } - - } - - - - -
- @if (PageViewModel.SelectedApplication.Id?.InstanceId != null && PageViewModel.SelectedMeter != null && PageViewModel.SelectedInstrument != null) - { - - } - else if (PageViewModel.SelectedMeter != null) - { -

@PageViewModel.SelectedMeter.MeterName

- - - - - @context.Name - - - - - } - else - { -

@Loc[nameof(Dashboard.Resources.Metrics.MetricsSelectInstrument)]

- } + + + +
+
+ @if (PageViewModel.SelectedApplication.Id?.InstanceId != null && PageViewModel.SelectedMeter != null && PageViewModel.SelectedInstrument != null) + { + + } + else if (PageViewModel.SelectedMeter != null) + { +

@PageViewModel.SelectedMeter.MeterName

+ + + + + @context.Name + + + + + + } + else + { +

@Loc[nameof(Dashboard.Resources.Metrics.MetricsSelectInstrument)]

+ } +
+
+
+ + } + else if (PageViewModel.Instruments == null) + { +
+  @Loc[nameof(Dashboard.Resources.Metrics.MetricsSelectAResource)]
- - - } - else if (PageViewModel.Instruments == null) - { -
-  @Loc[nameof(Dashboard.Resources.Metrics.MetricsSelectAResource)] -
- } - else - { -
-  @Loc[nameof(Dashboard.Resources.Metrics.MetricsNoMetricsForResource)] + } + else + { +
+  @Loc[nameof(Dashboard.Resources.Metrics.MetricsNoMetricsForResource)] +
+ }
- } -
+ +
diff --git a/src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs b/src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs index c94088a4303..8dd32478686 100644 --- a/src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Dashboard.Components.Layout; using Aspire.Dashboard.Model; using Aspire.Dashboard.Model.Otlp; using Aspire.Dashboard.Otlp.Model; @@ -18,6 +19,7 @@ public partial class Metrics : IDisposable, IPageWithSessionAndUrlState _selectApplication = null!; private List> _durations = null!; private static readonly TimeSpan s_defaultDuration = TimeSpan.FromMinutes(5); + private AspirePageContentLayout? _contentLayout; private List _applications = default!; private List> _applicationViewModels = default!; @@ -155,12 +157,12 @@ private Task HandleSelectedApplicationChangedAsync() { PageViewModel.SelectedMeter = null; PageViewModel.SelectedInstrument = null; - return this.AfterViewModelChangedAsync(); + return this.AfterViewModelChangedAsync(_contentLayout, isChangeInToolbar: true); } private Task HandleSelectedDurationChangedAsync() { - return this.AfterViewModelChangedAsync(); + return this.AfterViewModelChangedAsync(_contentLayout, isChangeInToolbar: true); } public sealed class MetricsViewModel @@ -207,7 +209,7 @@ private Task HandleSelectedTreeItemChangedAsync() PageViewModel.SelectedInstrument = null; } - return this.AfterViewModelChangedAsync(); + return this.AfterViewModelChangedAsync(_contentLayout, isChangeInToolbar: false); } public string GetUrlFromSerializableViewModel(MetricsPageState serializable) @@ -229,7 +231,7 @@ public string GetUrlFromSerializableViewModel(MetricsPageState serializable) private async Task OnViewChangedAsync(MetricViewKind newView) { PageViewModel.SelectedViewKind = newView; - await this.AfterViewModelChangedAsync(); + await this.AfterViewModelChangedAsync(_contentLayout, isChangeInToolbar: false); } private void UpdateSubscription() diff --git a/src/Aspire.Dashboard/Components/Pages/Metrics.razor.css b/src/Aspire.Dashboard/Components/Pages/Metrics.razor.css index a6799848fa3..fb2f74cb684 100644 --- a/src/Aspire.Dashboard/Components/Pages/Metrics.razor.css +++ b/src/Aspire.Dashboard/Components/Pages/Metrics.razor.css @@ -70,37 +70,6 @@ margin-left: 10px; } -::deep.metrics-layout { - display: grid; - grid-template-rows: auto auto 1fr; - height: 100%; - width: 100%; - grid-template-areas: - "head" - "toolbar" - "main"; - gap: calc(var(--design-unit) * 2px); -} - -::deep.metrics-layout > h1 { - grid-area: head; -} - -::deep.metrics-layout > fluent-toolbar { - grid-area: toolbar; - padding: var(--layout-toolbar-padding); -} - -::deep.metrics-layout > .page-content-area { - grid-area: main; -} - -.page-content-area { - height: 100%; - width: 100%; - overflow: auto; -} - ::deep #plotly-chart-container { margin-left: -40px; } diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor b/src/Aspire.Dashboard/Components/Pages/Resources.razor index 3b3d6765a14..b4258d227bf 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor @@ -10,104 +10,124 @@ -
- -

@Loc[nameof(Dashboard.Resources.Resources.ResourcesHeader)]

- - -
+@{ + var showDetailsView = SelectedResource is not null; +} - -
@Loc[nameof(Dashboard.Resources.Resources.ResourcesResourceTypesHeader)]
- - - - @foreach (var (resourceType, _) in _allResourceTypes) - { - var isChecked = _visibleResourceTypes.ContainsKey(resourceType); - - } - - -
+
+ + +

@Loc[nameof(Dashboard.Resources.Resources.ResourcesHeader)]

+
- - - @{ - var gridTemplateColumns = HasResourcesWithCommands ? "1fr 1.5fr 1.25fr 1.5fr 2.5fr 2.5fr 1fr 1fr 1fr" : "1fr 1.5fr 1.25fr 1.5fr 2.5fr 2.5fr 1fr 1fr"; + + @if (ViewportInformation.IsDesktop) + { + } - - - - - - - - - - - - - - @if (GetSourceColumnValueAndTooltip(context) is { } columnDisplay) - { - - } - - - - - - @ControlsStringsLoc[ControlsStrings.ViewAction] - - - @{ - var id = $"details-button-{context.Uid}"; - } - @ControlsStringsLoc[nameof(ControlsStrings.ViewAction)] - - @if (HasResourcesWithCommands) - { - - - + else + { +
+
@Loc[nameof(Dashboard.Resources.Resources.ResourcesResourceTypesHeader)]
+ + +
+ } + + +
+ + + +
@Loc[nameof(Dashboard.Resources.Resources.ResourcesResourceTypesHeader)]
+ + + +
+ + + + @{ + var gridTemplateColumns = HasResourcesWithCommands ? "1fr 1.5fr 1.25fr 1.5fr 2.5fr 2.5fr 1fr 1fr 1fr" : "1fr 1.5fr 1.25fr 1.5fr 2.5fr 2.5fr 1fr 1fr"; } - - -  @Loc[nameof(Dashboard.Resources.Resources.ResourcesNoResources)] - - - -
- -
-
+ + + + + + + + + + + + + + @if (GetSourceColumnValueAndTooltip(context) is { } columnDisplay) + { + + } + + + + + + @ControlsStringsLoc[ControlsStrings.ViewAction] + + + @{ + var id = $"details-button-{context.Uid}"; + } + @ControlsStringsLoc[nameof(ControlsStrings.ViewAction)] + + @if (HasResourcesWithCommands) + { + + + + } + + +  @Loc[nameof(Dashboard.Resources.Resources.ResourcesNoResources)] + + +
+
+ +
+
+ +
diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs index de87ce666cb..d6dbe932685 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs @@ -6,12 +6,14 @@ using System.Globalization; using System.Text; using Aspire.Dashboard.Extensions; +using Aspire.Dashboard.Components.Resize; using Aspire.Dashboard.Model; using Aspire.Dashboard.Otlp.Model; using Aspire.Dashboard.Otlp.Storage; using Aspire.Dashboard.Resources; using Aspire.Dashboard.Utils; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.JSInterop; @@ -36,27 +38,31 @@ public partial class Resources : ComponentBase, IAsyncDisposable public required BrowserTimeProvider TimeProvider { get; init; } [Inject] public required IJSRuntime JS { get; init; } + [Inject] + public required ProtectedSessionStorage SessionStorage { get; init; } + + [CascadingParameter] + public required ViewportInformation ViewportInformation { get; init; } + + [Parameter] + [SupplyParameterFromQuery] + public string? VisibleTypes { get; set; } private ResourceViewModel? SelectedResource { get; set; } private readonly CancellationTokenSource _watchTaskCancellationTokenSource = new(); private readonly ConcurrentDictionary _resourceByName = new(StringComparers.ResourceName); private readonly ConcurrentDictionary _allResourceTypes = []; - private readonly ConcurrentDictionary _visibleResourceTypes; + private readonly ConcurrentDictionary _visibleResourceTypes = new(StringComparers.ResourceName); private string _filter = ""; private bool _isTypeFilterVisible; private Task? _resourceSubscriptionTask; private bool _isLoading = true; private string? _elementIdBeforeDetailsViewOpened; - public Resources() - { - _visibleResourceTypes = new(StringComparers.ResourceType); - } - private bool Filter(ResourceViewModel resource) => _visibleResourceTypes.ContainsKey(resource.ResourceType) && (_filter.Length == 0 || resource.MatchesFilter(_filter)) && !resource.IsHiddenState(); - protected Task OnResourceTypeVisibilityChangedAsync(string resourceType, bool isVisible) + private Task OnResourceTypeVisibilityChangedAsync(string resourceType, bool isVisible) { if (isVisible) { @@ -85,7 +91,7 @@ static bool SetEqualsKeys(ConcurrentDictionary left, ConcurrentDic var keysLeft = left.Keys; var keysRight = right.Keys; - return keysLeft.Count == keysRight.Count && keysLeft.SequenceEqual(keysRight, StringComparers.ResourceType); + return keysLeft.Count == keysRight.Count && keysLeft.OrderBy(key => key, StringComparers.ResourceType).SequenceEqual(keysRight.OrderBy(key => key, StringComparers.ResourceType), StringComparers.ResourceType); } return SetEqualsKeys(_visibleResourceTypes, _allResourceTypes) @@ -115,6 +121,8 @@ static bool UnionWithKeys(ConcurrentDictionary left, ConcurrentDic { _visibleResourceTypes.Clear(); } + + StateHasChanged(); } } @@ -151,6 +159,8 @@ protected override async Task OnInitializedAsync() async Task SubscribeResourcesAsync() { + var preselectedVisibleResourceTypes = VisibleTypes?.Split(',').ToHashSet(); + var (snapshot, subscription) = await DashboardClient.SubscribeResourcesAsync(_watchTaskCancellationTokenSource.Token); // Apply snapshot. @@ -159,7 +169,11 @@ async Task SubscribeResourcesAsync() var added = _resourceByName.TryAdd(resource.Name, resource); _allResourceTypes.TryAdd(resource.ResourceType, true); - _visibleResourceTypes.TryAdd(resource.ResourceType, true); + + if (preselectedVisibleResourceTypes is null || preselectedVisibleResourceTypes.Contains(resource.ResourceType)) + { + _visibleResourceTypes.TryAdd(resource.ResourceType, true); + } Debug.Assert(added, "Should not receive duplicate resources in initial snapshot data."); } @@ -227,6 +241,8 @@ private async Task ClearSelectedResourceAsync(bool causedByUserAction = false) { SelectedResource = null; + await InvokeAsync(StateHasChanged); + if (_elementIdBeforeDetailsViewOpened is not null && causedByUserAction) { await JS.InvokeVoidAsync("focusElement", _elementIdBeforeDetailsViewOpened); diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor.css b/src/Aspire.Dashboard/Components/Pages/Resources.razor.css index b188bb88443..2528e434321 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor.css +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor.css @@ -1,16 +1,7 @@ -::deep .resource-state-badge { - padding: 0 3px; - cursor: pointer; -} - ::deep fluent-toolbar > h1 { padding-left: calc(var(--design-unit) * 1.5px); } -::deep.content-layout-with-toolbar > fluent-toolbar { - padding: var(--layout-toolbar-padding); -} - ::deep .unread-logs-errors-link { vertical-align: middle; --unread-logs-badge-color: #ffffff; diff --git a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor index 6c5e42d9881..ef7890e5dd6 100644 --- a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor +++ b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor @@ -16,110 +16,140 @@ -
-

@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsHeader)]

- - - - -
- @Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsLevels)] - - -
- - @Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsFilters)] - @if (ViewModel.Filters.Count == 0) - { - @Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsNoFilters)] - } - else - { - foreach (var filter in ViewModel.Filters) +
+ + +

@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsHeader)]

+
+ + + + + @if (ViewportInformation.IsDesktop) { - @filter.GetDisplayText(LogsLoc) - +
+ @Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsLevels)] + + + +
+ } + else + { + } - } - - - - - @{ - var eventName = OtlpHelpers.GetValue(SelectedLogEntry!.LogEntry.Attributes, "event.name") - ?? OtlpHelpers.GetValue(SelectedLogEntry.LogEntry.Attributes, "logrecord.event.name") - ?? Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsEntryDetails)]; + + @Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsFilters)] + @if (ViewModel.Filters.Count == 0) + { + @Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsNoFilters)] + } + else + { + foreach (var filter in ViewModel.Filters) + { + @filter.GetDisplayText(LogsLoc) + + } } -
- @eventName - @SelectedLogEntry.LogEntry.Scope.ScopeName -
-
- -
-
- - - - - @GetResourceName(context.Application) - - - - - - - @FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, context.TimeStamp, MillisecondsDisplay.Truncated) - - - - - - @if (!string.IsNullOrEmpty(context.TraceId)) - { - - @OtlpHelpers.ToShortenedId(context.TraceId) - - } - - - @{ - var id = $"details-button-{context.InternalId}"; - } - @ControlsStringsLoc[nameof(ControlsStrings.ViewAction)] - - - -  @Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsNoLogsFound)] - - -
- -
-
-
- -
-
+ @if (ViewportInformation.IsDesktop) + { + + } + else + { + + @Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsAddFilter)] + + } +
+ + + + + @{ + var eventName = OtlpHelpers.GetValue(context!.LogEntry.Attributes, "event.name") + ?? OtlpHelpers.GetValue(context!.LogEntry.Attributes, "logrecord.event.name") + ?? Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsEntryDetails)]; + } + +
+ @eventName + @context!.LogEntry.Scope.ScopeName +
+
+ +
+
+ + + + + @GetResourceName(context.Application) + + + + + + + @FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, context.TimeStamp, MillisecondsDisplay.Truncated) + + + + + + @if (!string.IsNullOrEmpty(context.TraceId)) + { + + @OtlpHelpers.ToShortenedId(context.TraceId) + + } + + + @{ + var id = $"details-button-{context.InternalId}"; + } + @ControlsStringsLoc[nameof(ControlsStrings.ViewAction)] + + + +  @Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsNoLogsFound)] + + +
+
+
+
+ +
+
+
+ + + + +
diff --git a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs index dc03f7e49f0..06691246ceb 100644 --- a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs @@ -4,6 +4,8 @@ using System.Globalization; using Aspire.Dashboard.Components.Dialogs; using Aspire.Dashboard.Configuration; +using Aspire.Dashboard.Components.Layout; +using Aspire.Dashboard.Components.Resize; using Aspire.Dashboard.Extensions; using Aspire.Dashboard.Model; using Aspire.Dashboard.Model.Otlp; @@ -31,6 +33,8 @@ public partial class StructuredLogs : IPageWithSessionAndUrlState DashboardUrls.StructuredLogsBasePath; public string SessionStorageKey => "StructuredLogs_PageState"; @@ -60,6 +64,12 @@ public partial class StructuredLogs : IPageWithSessionAndUrlState Logger { get; init; } + [Inject] + public required DimensionManager DimensionManager { get; set; } + + [CascadingParameter] + public required ViewportInformation ViewportInformation { get; set; } + [Inject] public required IOptions DashboardOptions { get; init; } @@ -117,11 +127,17 @@ protected override Task OnInitializedAsync() { if (!string.IsNullOrEmpty(TraceId)) { - ViewModel.AddFilter(new LogFilter { Field = "TraceId", Condition = FilterCondition.Equals, Value = TraceId }); + ViewModel.AddFilter(new LogFilter + { + Field = "TraceId", Condition = FilterCondition.Equals, Value = TraceId + }); } if (!string.IsNullOrEmpty(SpanId)) { - ViewModel.AddFilter(new LogFilter { Field = "SpanId", Condition = FilterCondition.Equals, Value = SpanId }); + ViewModel.AddFilter(new LogFilter + { + Field = "SpanId", Condition = FilterCondition.Equals, Value = SpanId + }); } _allApplication = new() @@ -141,7 +157,11 @@ protected override Task OnInitializedAsync() new SelectViewModel { Id = LogLevel.Critical, Name = "Critical" }, }; - PageViewModel = new StructuredLogsPageViewModel { SelectedLogLevel = _logLevels[0], SelectedApplication = _allApplication }; + PageViewModel = new StructuredLogsPageViewModel + { + SelectedLogLevel = _logLevels[0], + SelectedApplication = _allApplication + }; UpdateApplications(); _applicationsSubscription = TelemetryRepository.OnNewApplications(() => InvokeAsync(() => @@ -170,7 +190,7 @@ private Task HandleSelectedApplicationChangedAsync() { _applicationChanged = true; - return this.AfterViewModelChangedAsync(); + return this.AfterViewModelChangedAsync(_contentLayout, isChangeInToolbar: true); } private async Task HandleSelectedLogLevelChangedAsync() @@ -178,7 +198,7 @@ private async Task HandleSelectedLogLevelChangedAsync() _applicationChanged = true; await ClearSelectedLogEntryAsync(); - await this.AfterViewModelChangedAsync(); + await this.AfterViewModelChangedAsync(_contentLayout, isChangeInToolbar: true); } private void UpdateSubscription() @@ -228,6 +248,11 @@ private async Task ClearSelectedLogEntryAsync(bool causedByUserAction = false) private async Task OpenFilterAsync(LogFilter? entry) { + if (_contentLayout is not null) + { + await _contentLayout.CloseMobileToolbarAsync(); + } + var logPropertyKeys = TelemetryRepository.GetLogPropertyKeys(PageViewModel.SelectedApplication.Id?.GetApplicationKey()); var title = entry is not null ? Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsEditFilter)] : Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsAddFilter)]; @@ -241,8 +266,7 @@ private async Task OpenFilterAsync(LogFilter? entry) }; var data = new FilterDialogViewModel { - Filter = entry, - LogPropertyKeys = logPropertyKeys + Filter = entry, LogPropertyKeys = logPropertyKeys }; await DialogService.ShowPanelAsync(data, parameters); } @@ -263,14 +287,13 @@ private async Task HandleFilterDialog(DialogResult result) await ClearSelectedLogEntryAsync(); } - await this.AfterViewModelChangedAsync(); + await this.AfterViewModelChangedAsync(_contentLayout, isChangeInToolbar: true); } private void HandleFilter(ChangeEventArgs args) { if (args.Value is string newFilter) { - PageViewModel.Filter = newFilter; _filterCts?.Cancel(); // Debouncing logic. Apply the filter after a delay. @@ -286,16 +309,23 @@ private void HandleFilter(ChangeEventArgs args) } } - private async Task HandleClearAsync() + private async Task HandleAfterFilterBindAsync() { + if (!string.IsNullOrEmpty(_filter)) + { + return; + } + if (_filterCts is not null) { await _filterCts.CancelAsync(); } ViewModel.FilterText = string.Empty; + await ClearSelectedLogEntryAsync(); - StateHasChanged(); + await InvokeAsync(StateHasChanged); + await this.AfterViewModelChangedAsync(_contentLayout, true); } private string GetResourceName(OtlpApplication app) => OtlpApplication.GetResourceName(app, _applications); @@ -322,14 +352,25 @@ protected override async Task OnAfterRenderAsync(bool firstRender) if (firstRender) { await JS.InvokeVoidAsync("initializeContinuousScroll"); + DimensionManager.OnBrowserDimensionsChanged += OnBrowserResize; } } + private void OnBrowserResize(object? o, EventArgs args) + { + InvokeAsync(async () => + { + await JS.InvokeVoidAsync("resetContinuousScrollPosition"); + await JS.InvokeVoidAsync("initializeContinuousScroll"); + }); + } + public void Dispose() { _applicationsSubscription?.Dispose(); _logsSubscription?.Dispose(); _filterCts?.Dispose(); + DimensionManager.OnBrowserDimensionsChanged -= OnBrowserResize; } public string GetUrlFromSerializableViewModel(StructuredLogsPageState serializable) @@ -348,7 +389,6 @@ public StructuredLogsPageState ConvertViewModelToSerializable() { return new StructuredLogsPageState { - Filter = PageViewModel.Filter, LogLevelText = PageViewModel.SelectedLogLevel.Id?.ToString().ToLowerInvariant(), SelectedApplication = PageViewModel.SelectedApplication.Id is not null ? PageViewModel.SelectedApplication.Name : null, Filters = ViewModel.Filters @@ -357,7 +397,7 @@ public StructuredLogsPageState ConvertViewModelToSerializable() public void UpdateViewModelFromQuery(StructuredLogsPageViewModel viewModel) { - PageViewModel.SelectedApplication = _applicationViewModels.GetApplication(Logger, ApplicationName, _allApplication); + viewModel.SelectedApplication = _applicationViewModels.GetApplication(Logger, ApplicationName, _allApplication); ViewModel.ApplicationKey = PageViewModel.SelectedApplication.Id?.GetApplicationKey(); if (LogLevelText is not null && Enum.TryParse(LogLevelText, ignoreCase: true, out var logLevel)) @@ -390,14 +430,12 @@ public void UpdateViewModelFromQuery(StructuredLogsPageViewModel viewModel) public class StructuredLogsPageViewModel { - public string Filter { get; set; } = string.Empty; public required SelectViewModel SelectedApplication { get; set; } public SelectViewModel SelectedLogLevel { get; set; } = default!; } public class StructuredLogsPageState { - public required string Filter { get; set; } public string? SelectedApplication { get; set; } public string? LogLevelText { get; set; } public required IReadOnlyCollection Filters { get; set; } diff --git a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.css b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.css index 5c693e7cbee..62f5fbda1a3 100644 --- a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.css +++ b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.css @@ -1,28 +1,3 @@ -::deep.logs-layout { - display: grid; - grid-template-rows: auto auto 1fr; - height: 100%; - width: 100%; - grid-template-areas: - "head" - "toolbar" - "main"; - gap: calc(var(--design-unit) * 2px); -} - -::deep.logs-layout > h1 { - grid-area: head; -} - -::deep.logs-layout > fluent-toolbar { - grid-area: toolbar; - padding: var(--layout-toolbar-padding); -} - -::deep.logs-layout > .summary-details-container { - grid-area: main; -} - ::deep .log-row-critical, ::deep .log-row-critical fluent-button[appearance=lightweight]:not(:hover)::part(control) { background-color: var(--log-critical); @@ -54,10 +29,6 @@ overflow: auto; } -::deep .logs-summary-layout > .total-items-footer { - grid-area: foot; -} - ::deep .wrap { text-wrap: wrap; white-space: pre-line; diff --git a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor index ae5b0876f38..f6fa7c1e4cd 100644 --- a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor +++ b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor @@ -10,186 +10,194 @@ -@if (_trace is { } trace) -{ -
- - -
- @Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailTraceStartHeader)] @FormatHelpers.FormatDateTime(TimeProvider, _trace.FirstSpan.StartTime, MillisecondsDisplay.Truncated) -
- -
- @Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailDurationHeader)] @DurationFormatter.FormatDuration(trace.Duration) -
- -
- @Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailResourcesHeader)] @trace.Spans.GroupBy(s => s.Source).Count() -
- -
- @Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailDepthHeader)] @_maxDepth -
- -
- @Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailTotalSpansHeader)] @trace.Spans.Count -
- @ControlStringsLoc[nameof(ControlsStrings.ViewLogsLink)] -
- - - - @{ var shortedSpanId = OtlpHelpers.ToShortenedId(SelectedSpan!.Span.SpanId); } -
- @SelectedSpan!.Title - @shortedSpanId +
+ @if (_trace is { } trace) + { + + + + + +
+ @Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailTraceStartHeader)] @FormatHelpers.FormatDateTime(TimeProvider, _trace.FirstSpan.StartTime, MillisecondsDisplay.Truncated) +
+ +
+ @Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailDurationHeader)] @DurationFormatter.FormatDuration(trace.Duration) +
+ +
+ @Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailResourcesHeader)] @trace.Spans.GroupBy(s => s.Source).Count()
- - - - -
- @{ - var isServerOrConsumer = context.Span.Kind == OtlpSpanKind.Server || context.Span.Kind == OtlpSpanKind.Consumer; - // Indent the span name based on the depth of the span. - var marginLeft = (context.Depth - 1) * 15; + +
+ @Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailDepthHeader)] @_maxDepth +
+ +
+ @Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailTotalSpansHeader)] @trace.Spans.Count +
+ @ControlStringsLoc[nameof(ControlsStrings.ViewLogsLink)] + + + + + @{ var shortedSpanId = OtlpHelpers.ToShortenedId(context!.Span.SpanId); } +
+ @context!.Title + @shortedSpanId +
+
+ + + +
+ @{ + var isServerOrConsumer = context.Span.Kind == OtlpSpanKind.Server || context.Span.Kind == OtlpSpanKind.Consumer; + // Indent the span name based on the depth of the span. + var marginLeft = (context.Depth - 1) * 15; - // We want to have consistent margin for both client and server spans. - string spanNameContainerStyle; - if (!isServerOrConsumer) - { + // We want to have consistent margin for both client and server spans. + string spanNameContainerStyle; + if (!isServerOrConsumer) + { // Client span has 19px extra content: // - 5px extra margin-left // - 5px border // - 9px padding-left spanNameContainerStyle = $"margin-left: 5px; border-left-color: {ColorGenerator.Instance.GetColorHexByKey(GetResourceName(context.Span.Source))}; border-left-width: 5px; border-left-style: solid; padding-left: 9px;"; - } - else - { + } + else + { // Span with icon has 19px extra content: // - 16px icon // - 3px padding-left spanNameContainerStyle = string.Empty; - } - } + } + } - - - @if (context.Children.Count > 0) - { + + + @if (context.Children.Count > 0) + { @(context.IsCollapsed ? '+' : '-') - } - - - @if (isServerOrConsumer) - { + } + + + @if (isServerOrConsumer) + { - } + } - @if (context.IsError) - { + @if (context.IsError) + { - } - @GetResourceName(context.Span.Source) - @if (context.HasUninstrumentedPeer) - { + } + @GetResourceName(context.Span.Source) + @if (context.HasUninstrumentedPeer) + { - @{ - Icon icon; - if (context.Span.Attributes.HasKey("db.system")) - { - icon = new Icons.Filled.Size16.Database(); - } - else if (context.Span.Attributes.HasKey("messaging.system")) - { - icon = new Icons.Filled.Size16.Mail(); - } - else - { - // Everything else. - icon = new Icons.Filled.Size16.ArrowCircleRight(); - } + @{ + Icon icon; + if (context.Span.Attributes.HasKey("db.system")) + { + icon = new Icons.Filled.Size16.Database(); + } + else if (context.Span.Attributes.HasKey("messaging.system")) + { + icon = new Icons.Filled.Size16.Mail(); + } + else + { + // Everything else. + icon = new Icons.Filled.Size16.ArrowCircleRight(); + } } @context.UninstrumentedPeer - - } - @SpanWaterfallViewModel.GetDisplaySummary(context.Span) + + } + @SpanWaterfallViewModel.GetDisplaySummary(context.Span) + - -
-
- - -
-
- @DurationFormatter.FormatDuration(TimeSpan.Zero) +
+
+ + +
+
+ @DurationFormatter.FormatDuration(TimeSpan.Zero) -
- @DurationFormatter.FormatDuration(trace.Duration / 4) +
+ @DurationFormatter.FormatDuration(trace.Duration / 4) -
- @DurationFormatter.FormatDuration(trace.Duration / 4 * 2) +
+ @DurationFormatter.FormatDuration(trace.Duration / 4 * 2) -
- @DurationFormatter.FormatDuration(trace.Duration / 4 * 3) +
+ @DurationFormatter.FormatDuration(trace.Duration / 4 * 3) - @DurationFormatter.FormatDuration(trace.Duration) -
-
-
- -
-
-
-
- @SpanWaterfallViewModel.GetTitle(context.Span, _applications) - @DurationFormatter.FormatDuration(context.Span.Duration) + @DurationFormatter.FormatDuration(trace.Duration) +
-
-
-
-
-
-
-
-
-
- - @{ - var id = context.Span.SpanId; - } + + +
+
+
+
+ @SpanWaterfallViewModel.GetTitle(context.Span, _applications) + @DurationFormatter.FormatDuration(context.Span.Duration) +
+
+
+
+
+
+
+
+
+
+ + @{ + var id = context.Span.SpanId; + } - @ControlStringsLoc[nameof(ControlsStrings.ViewAction)] - -
-
-
- -
-
-
-} -else -{ -
-   @string.Format(Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailTraceNotFound)], TraceId) -
-} + @ControlStringsLoc[nameof(ControlsStrings.ViewAction)] +
+
+
+
+ +
+ + +
+ } + else + { +
+   @string.Format(Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailTraceNotFound)], TraceId) +
+ } +
diff --git a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.css b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.css index 85ee9147f8d..ae3c369cd03 100644 --- a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.css +++ b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.css @@ -157,31 +157,6 @@ color: var(--neutral-foreground-rest); } -::deep.trace-detail-layout { - display: grid; - grid-template-rows: auto auto 1fr; - height: 100%; - width: 100%; - grid-template-areas: - "head" - "toolbar" - "main"; - gap: calc(var(--design-unit) * 2px); -} - -::deep.trace-detail-layout > .trace-detail-header { - grid-area: head; -} - -::deep.trace-detail-layout > fluent-toolbar { - grid-area: toolbar; - padding: var(--layout-toolbar-padding); -} - -::deep.trace-detail-layout > .summary-details-container { - grid-area: main; -} - ::deep .pane-details-title { text-overflow: ellipsis; white-space: nowrap; diff --git a/src/Aspire.Dashboard/Components/Pages/Traces.razor b/src/Aspire.Dashboard/Components/Pages/Traces.razor index 756b41dd240..ad5cbc3a8b2 100644 --- a/src/Aspire.Dashboard/Components/Pages/Traces.razor +++ b/src/Aspire.Dashboard/Components/Pages/Traces.razor @@ -1,12 +1,10 @@ @page "/traces" @page "/traces/resource/{applicationName}" -@using Aspire.Dashboard.Model.Otlp @using Aspire.Dashboard.Otlp.Model @using Aspire.Dashboard.Resources @using Aspire.Dashboard.Utils @using System.Globalization -@inject NavigationManager NavigationManager @inject IJSRuntime JS @inject IStringLocalizer Loc @inject IStringLocalizer ControlsStringsLoc @@ -14,87 +12,102 @@ -
-

@Loc[nameof(Dashboard.Resources.Traces.TracesHeader)]

- - - - -
- - - - @FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, context.FirstSpan.StartTime, MillisecondsDisplay.Truncated) - - - - @OtlpHelpers.ToShortenedId(context.TraceId) - - - - - @foreach (var item in context.Spans.GroupBy(s => s.Source).OrderBy(g => g.Min(s => s.StartTime))) - { - - - @if (item.Any(s => s.Status == OtlpSpanStatusCode.Error)) - { +
+ + +

@Loc[nameof(Dashboard.Resources.Traces.TracesHeader)]

+
+ + + + + +
+ + + + @FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, context.FirstSpan.StartTime, MillisecondsDisplay.Truncated) + + + + @OtlpHelpers.ToShortenedId(context.TraceId) + + + + + @foreach (var item in context.Spans.GroupBy(s => s.Source).OrderBy(g => g.Min(s => s.StartTime))) + { + + + @if (item.Any(s => s.Status == OtlpSpanStatusCode.Error)) + { + } + @GetResourceName(item.Key) (@item.Count()) + + + } + + + + @($"+{overflow.ItemsOverflow.Count()}") + + + + @{ + var items = overflow.ItemsOverflow.ToList(); + } + + @foreach (var item in items) + { +
+ @item.ChildContent +
} - @GetResourceName(item.Key) (@item.Count()) +
+
+
+
+ +
+ @if (ViewportInformation.IsDesktop) + { + + + @DurationFormatter.FormatDuration(context.Duration) - - } - - - - @($"+{overflow.ItemsOverflow.Count()}") - - - - @{ - var items = overflow.ItemsOverflow.ToList(); - } - - @foreach (var item in items) + } + else { -
- @item.ChildContent -
+ @DurationFormatter.FormatDuration(context.Duration) } -
-
- - - -
- - - @DurationFormatter.FormatDuration(context.Duration) - -
-
+
+
- - @ControlsStringsLoc[nameof(ControlsStrings.ViewAction)] - -
- -  @Loc[nameof(Dashboard.Resources.Traces.TracesNoTraces)] - -
-
- + + @ControlsStringsLoc[nameof(ControlsStrings.ViewAction)] + + + +  @Loc[nameof(Dashboard.Resources.Traces.TracesNoTraces)] + + +
+ + + + +
diff --git a/src/Aspire.Dashboard/Components/Pages/Traces.razor.cs b/src/Aspire.Dashboard/Components/Pages/Traces.razor.cs index 30c57999dca..54831ca0c60 100644 --- a/src/Aspire.Dashboard/Components/Pages/Traces.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Traces.razor.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using Aspire.Dashboard.Components.Resize; using Aspire.Dashboard.Configuration; using Aspire.Dashboard.Model; using Aspire.Dashboard.Model.Otlp; @@ -10,6 +11,7 @@ using Aspire.Dashboard.Resources; using Aspire.Dashboard.Utils; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; using Microsoft.Extensions.Options; using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.JSInterop; @@ -54,6 +56,18 @@ public partial class Traces [Inject] public required ILogger Logger { get; init; } + [Inject] + public required ProtectedSessionStorage SessionStorage { get; init; } + + [Inject] + public required NavigationManager NavigationManager { get; init; } + + [Inject] + public required DimensionManager DimensionManager { get; set; } + + [CascadingParameter] + public required ViewportInformation ViewportInformation { get; set; } + private string GetNameTooltip(OtlpTrace trace) { var tooltip = string.Format(CultureInfo.InvariantCulture, Loc[nameof(Dashboard.Resources.Traces.TracesFullName)], trace.FullName); @@ -133,7 +147,7 @@ private void UpdateApplications() UpdateSubscription(); } - private Task HandleSelectedApplicationChangedAsync() + private Task HandleSelectedApplicationChanged() { NavigationManager.NavigateTo(DashboardUrls.TracesUrl(resource: _selectedApplication.Name)); _applicationChanged = true; @@ -161,7 +175,6 @@ private void HandleFilter(ChangeEventArgs args) { if (args.Value is string newFilter) { - _filter = newFilter; _filterCts?.Cancel(); // Debouncing logic. Apply the filter after a delay. @@ -175,11 +188,20 @@ private void HandleFilter(ChangeEventArgs args) } } - private void HandleClear() + private async Task HandleAfterFilterBindAsync() { - _filterCts?.Cancel(); + if (!string.IsNullOrEmpty(_filter)) + { + return; + } + + if (_filterCts is not null) + { + await _filterCts.CancelAsync(); + } + TracesViewModel.FilterText = string.Empty; - StateHasChanged(); + await InvokeAsync(StateHasChanged); } private string GetResourceName(OtlpApplication app) => OtlpApplication.GetResourceName(app, _applications); @@ -194,12 +216,23 @@ protected override async Task OnAfterRenderAsync(bool firstRender) if (firstRender) { await JS.InvokeVoidAsync("initializeContinuousScroll"); + DimensionManager.OnBrowserDimensionsChanged += OnBrowserResize; } } + private void OnBrowserResize(object? o, EventArgs args) + { + InvokeAsync(async () => + { + await JS.InvokeVoidAsync("resetContinuousScrollPosition"); + await JS.InvokeVoidAsync("initializeContinuousScroll"); + }); + } + public void Dispose() { _applicationsSubscription?.Dispose(); _tracesSubscription?.Dispose(); + DimensionManager.OnBrowserDimensionsChanged -= OnBrowserResize; } } diff --git a/src/Aspire.Dashboard/Components/Pages/Traces.razor.css b/src/Aspire.Dashboard/Components/Pages/Traces.razor.css index bafa76ed529..fdc864418d8 100644 --- a/src/Aspire.Dashboard/Components/Pages/Traces.razor.css +++ b/src/Aspire.Dashboard/Components/Pages/Traces.razor.css @@ -19,36 +19,6 @@ margin-right: 7px; } -::deep.traces-layout { - display: grid; - grid-template-rows: auto auto 1fr auto; - height: 100%; - width: 100%; - grid-template-areas: - "head" - "toolbar" - "main" - "foot"; - gap: calc(var(--design-unit) * 2px); -} - -::deep.traces-layout > h1 { - grid-area: head; -} - -::deep.traces-layout > fluent-toolbar { - grid-area: toolbar; - padding: var(--layout-toolbar-padding); -} - -::deep.traces-layout > .datagrid-overflow-area { - grid-area: main; -} - -::deep.traces-layout > .total-items-footer { - grid-area: foot; -} - ::deep fluent-progress-ring::part(background) { stroke: var(--neutral-fill-input-alt-active); } diff --git a/src/Aspire.Dashboard/Components/Resize/BrowserDimensionWatcher.razor.cs b/src/Aspire.Dashboard/Components/Resize/BrowserDimensionWatcher.razor.cs new file mode 100644 index 00000000000..b2f590097e6 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Resize/BrowserDimensionWatcher.razor.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace Aspire.Dashboard.Components.Resize; + +public class BrowserDimensionWatcher : ComponentBase +{ + [Parameter] + public ViewportInformation? ViewportInformation { get; set; } + + [Parameter] + public EventCallback ViewportInformationChanged { get; set; } + + [Inject] + public required IJSRuntime JS { get; init; } + + [Inject] + public required DimensionManager DimensionManager { get; init; } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + var viewport = await JS.InvokeAsync("window.getWindowDimensions"); + ViewportInformation = GetViewportInformation(viewport); + await ViewportInformationChanged.InvokeAsync(ViewportInformation); + + await JS.InvokeVoidAsync("window.listenToWindowResize", DotNetObjectReference.Create(this)); + } + + await base.OnAfterRenderAsync(firstRender); + } + + [JSInvokable] + public async Task OnResizeAsync(ViewportSize viewportSize) + { + var newViewportInformation = GetViewportInformation(viewportSize); + + if (newViewportInformation.IsDesktop != ViewportInformation!.IsDesktop || newViewportInformation.IsUltraLowHeight != ViewportInformation.IsUltraLowHeight) + { + ViewportInformation = newViewportInformation; + DimensionManager.IsResizing = true; + + await ViewportInformationChanged.InvokeAsync(newViewportInformation); + DimensionManager.InvokeOnBrowserDimensionsChanged(); + + DimensionManager.IsResizing = false; + } + } + + private static ViewportInformation GetViewportInformation(ViewportSize viewportSize) + { + return new ViewportInformation(IsDesktop: viewportSize.Width > 768, IsUltraLowHeight: viewportSize.Height < 400); + } + + public static ViewportInformation Create(int height, int width) + { + return new ViewportInformation(IsDesktop: width > 768, IsUltraLowHeight: height < 400); + } + + public record ViewportSize(int Width, int Height); +} diff --git a/src/Aspire.Dashboard/Components/Resize/DimensionManager.cs b/src/Aspire.Dashboard/Components/Resize/DimensionManager.cs new file mode 100644 index 00000000000..f6a558e1e74 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Resize/DimensionManager.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Dashboard.Components.Resize; + +public class DimensionManager +{ + public event EventHandler? OnBrowserDimensionsChanged; + + public bool IsResizing { get; set; } + + internal void InvokeOnBrowserDimensionsChanged() + { + OnBrowserDimensionsChanged?.Invoke(this, EventArgs.Empty); + } +} diff --git a/src/Aspire.Dashboard/Components/Resize/ViewportInformation.cs b/src/Aspire.Dashboard/Components/Resize/ViewportInformation.cs new file mode 100644 index 00000000000..95921a5dae0 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Resize/ViewportInformation.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Dashboard.Components.Resize; + +/// Set our mobile cutoff at 768 pixels, which is ~medium tablet size +/// Ultra low height is users with very high zooms and/or very low resolutions, +/// where the height is significantly constrained. In these cases, the users need the entire main page content +/// (toolbar, title, main content, footer) to be scrollable, rather than just the main content. +/// +public record ViewportInformation(bool IsDesktop, bool IsUltraLowHeight); diff --git a/src/Aspire.Dashboard/Components/Routes.razor b/src/Aspire.Dashboard/Components/Routes.razor index 8123ab66fd5..1b2af60faf4 100644 --- a/src/Aspire.Dashboard/Components/Routes.razor +++ b/src/Aspire.Dashboard/Components/Routes.razor @@ -1,15 +1,30 @@ -@using Microsoft.AspNetCore.Components.Authorization +@using Aspire.Dashboard.Components.Resize @inject IStringLocalizer Loc - - - - - - - @Loc[nameof(Resources.Routes.NotFoundPageTitle)] - - - - - + + +@if (_viewportInformation is null) +{ + // prevent render until we've determined the browser viewport so that we don't have to re-render + // if we guess wrong + return; +} + + + + + + + + + @Loc[nameof(Resources.Routes.NotFoundPageTitle)] + + + + + + + +@code { + private ViewportInformation? _viewportInformation; +} diff --git a/src/Aspire.Dashboard/ConsoleLogs/LogEntries.cs b/src/Aspire.Dashboard/ConsoleLogs/LogEntries.cs index 3a3f6c517ef..94c99f50fbb 100644 --- a/src/Aspire.Dashboard/ConsoleLogs/LogEntries.cs +++ b/src/Aspire.Dashboard/ConsoleLogs/LogEntries.cs @@ -6,7 +6,7 @@ namespace Aspire.Dashboard.ConsoleLogs; -internal sealed class LogEntries +public sealed class LogEntries { private readonly List _logEntries = new(); diff --git a/src/Aspire.Dashboard/ConsoleLogs/LogEntry.cs b/src/Aspire.Dashboard/ConsoleLogs/LogEntry.cs index c3bc01f2a6d..7f2a3f373ec 100644 --- a/src/Aspire.Dashboard/ConsoleLogs/LogEntry.cs +++ b/src/Aspire.Dashboard/ConsoleLogs/LogEntry.cs @@ -6,7 +6,7 @@ namespace Aspire.Dashboard.Model; [DebuggerDisplay("Timestamp = {(Timestamp ?? ParentTimestamp),nq}, Content = {Content}")] -internal sealed partial class LogEntry +public sealed class LogEntry { public string? Content { get; set; } public DateTimeOffset? Timestamp { get; set; } @@ -19,7 +19,7 @@ internal sealed partial class LogEntry public int LineNumber { get; set; } } -internal enum LogEntryType +public enum LogEntryType { Default, Error diff --git a/src/Aspire.Dashboard/DashboardWebApplication.cs b/src/Aspire.Dashboard/DashboardWebApplication.cs index 95053e2808b..a311cf894a6 100644 --- a/src/Aspire.Dashboard/DashboardWebApplication.cs +++ b/src/Aspire.Dashboard/DashboardWebApplication.cs @@ -12,6 +12,7 @@ using Aspire.Dashboard.Authentication.OtlpApiKey; using Aspire.Dashboard.Authentication.OtlpConnection; using Aspire.Dashboard.Components; +using Aspire.Dashboard.Components.Resize; using Aspire.Dashboard.Configuration; using Aspire.Dashboard.Model; using Aspire.Dashboard.Otlp; @@ -174,6 +175,11 @@ public DashboardWebApplication( // Time zone is set by the browser. builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddLocalization(); builder.Services.AddAntiforgery(options => diff --git a/src/Aspire.Dashboard/Model/CurrentChartViewModel.cs b/src/Aspire.Dashboard/Model/CurrentChartViewModel.cs new file mode 100644 index 00000000000..cd71116b640 --- /dev/null +++ b/src/Aspire.Dashboard/Model/CurrentChartViewModel.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Dashboard.Model; + +public class CurrentChartViewModel +{ + public bool OnlyShowValueChangesInTable { get; set; } = true; + public bool ShowCounts { get; set; } + public List DimensionFilters { get; } = []; + public string? PreviousMeterName { get; set; } + public string? PreviousInstrumentName { get; set; } + +} diff --git a/src/Aspire.Dashboard/Model/CounterChartViewModel.cs b/src/Aspire.Dashboard/Model/DimensionFilterViewModel.cs similarity index 93% rename from src/Aspire.Dashboard/Model/CounterChartViewModel.cs rename to src/Aspire.Dashboard/Model/DimensionFilterViewModel.cs index 577a04140a8..3ef76912573 100644 --- a/src/Aspire.Dashboard/Model/CounterChartViewModel.cs +++ b/src/Aspire.Dashboard/Model/DimensionFilterViewModel.cs @@ -6,11 +6,6 @@ namespace Aspire.Dashboard.Model; -public class CounterChartViewModel -{ - public List DimensionFilters { get; } = new(); -} - [DebuggerDisplay("{DebuggerToString(),nq}")] public class DimensionFilterViewModel { diff --git a/src/Aspire.Dashboard/Model/LogViewerViewModel.cs b/src/Aspire.Dashboard/Model/LogViewerViewModel.cs new file mode 100644 index 00000000000..374f6bace44 --- /dev/null +++ b/src/Aspire.Dashboard/Model/LogViewerViewModel.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Dashboard.ConsoleLogs; + +namespace Aspire.Dashboard.Model; + +public class LogViewerViewModel +{ + public LogEntries LogEntries { get; } = new(); + public string? ResourceName { get; set; } + +} diff --git a/src/Aspire.Dashboard/Resources/Layout.Designer.cs b/src/Aspire.Dashboard/Resources/Layout.Designer.cs index cb91cf4e5e0..e82f493a2f7 100644 --- a/src/Aspire.Dashboard/Resources/Layout.Designer.cs +++ b/src/Aspire.Dashboard/Resources/Layout.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -12,46 +11,32 @@ namespace Aspire.Dashboard.Resources { using System; - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class Layout { - private static global::System.Resources.ResourceManager resourceMan; + private static System.Resources.ResourceManager resourceMan; - private static global::System.Globalization.CultureInfo resourceCulture; + private static System.Globalization.CultureInfo resourceCulture; - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Layout() { } - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + public static System.Resources.ResourceManager ResourceManager { get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Aspire.Dashboard.Resources.Layout", typeof(Layout).Assembly); + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Aspire.Dashboard.Resources.Layout", typeof(Layout).Assembly); resourceMan = temp; } return resourceMan; } } - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + public static System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -60,156 +45,111 @@ internal Layout() { } } - /// - /// Looks up a localized string similar to .NET Aspire. - /// - public static string MainLayoutAspire { + public static string MainLayoutAspireRepoLink { get { - return ResourceManager.GetString("MainLayoutAspire", resourceCulture); + return ResourceManager.GetString("MainLayoutAspireRepoLink", resourceCulture); } } - /// - /// Looks up a localized string similar to Help. - /// public static string MainLayoutAspireDashboardHelpLink { get { return ResourceManager.GetString("MainLayoutAspireDashboardHelpLink", resourceCulture); } } - /// - /// Looks up a localized string similar to .NET Aspire repo. - /// - public static string MainLayoutAspireRepoLink { + public static string MainLayoutLaunchSettings { get { - return ResourceManager.GetString("MainLayoutAspireRepoLink", resourceCulture); + return ResourceManager.GetString("MainLayoutLaunchSettings", resourceCulture); } } - /// - /// Looks up a localized string similar to Launch settings. - /// - public static string MainLayoutLaunchSettings { + public static string MainLayoutUnhandledErrorMessage { get { - return ResourceManager.GetString("MainLayoutLaunchSettings", resourceCulture); + return ResourceManager.GetString("MainLayoutUnhandledErrorMessage", resourceCulture); } } - /// - /// Looks up a localized string similar to Close. - /// - public static string MainLayoutSettingsDialogClose { + public static string MainLayoutUnhandledErrorReload { get { - return ResourceManager.GetString("MainLayoutSettingsDialogClose", resourceCulture); + return ResourceManager.GetString("MainLayoutUnhandledErrorReload", resourceCulture); } } - /// - /// Looks up a localized string similar to Settings. - /// public static string MainLayoutSettingsDialogTitle { get { return ResourceManager.GetString("MainLayoutSettingsDialogTitle", resourceCulture); } } - /// - /// Looks up a localized string similar to An unhandled error has occurred.. - /// - public static string MainLayoutUnhandledErrorMessage { + public static string MainLayoutSettingsDialogClose { get { - return ResourceManager.GetString("MainLayoutUnhandledErrorMessage", resourceCulture); + return ResourceManager.GetString("MainLayoutSettingsDialogClose", resourceCulture); } } - /// - /// Looks up a localized string similar to Reload. - /// - public static string MainLayoutUnhandledErrorReload { + public static string NavMenuResourcesTab { get { - return ResourceManager.GetString("MainLayoutUnhandledErrorReload", resourceCulture); + return ResourceManager.GetString("NavMenuResourcesTab", resourceCulture); } } - /// - /// Looks up a localized string similar to Untrusted apps can send telemetry to the dashboard.. - /// - public static string MessageTelemetryBody { + public static string NavMenuMonitoringTab { get { - return ResourceManager.GetString("MessageTelemetryBody", resourceCulture); + return ResourceManager.GetString("NavMenuMonitoringTab", resourceCulture); } } - /// - /// Looks up a localized string similar to More information. - /// - public static string MessageTelemetryLink { + public static string NavMenuConsoleLogsTab { get { - return ResourceManager.GetString("MessageTelemetryLink", resourceCulture); + return ResourceManager.GetString("NavMenuConsoleLogsTab", resourceCulture); } } - /// - /// Looks up a localized string similar to Telemetry endpoint is unsecured. - /// - public static string MessageTelemetryTitle { + public static string NavMenuStructuredLogsTab { get { - return ResourceManager.GetString("MessageTelemetryTitle", resourceCulture); + return ResourceManager.GetString("NavMenuStructuredLogsTab", resourceCulture); } } - /// - /// Looks up a localized string similar to Console. - /// - public static string NavMenuConsoleLogsTab { + public static string NavMenuTracesTab { get { - return ResourceManager.GetString("NavMenuConsoleLogsTab", resourceCulture); + return ResourceManager.GetString("NavMenuTracesTab", resourceCulture); } } - /// - /// Looks up a localized string similar to Metrics. - /// public static string NavMenuMetricsTab { get { return ResourceManager.GetString("NavMenuMetricsTab", resourceCulture); } } - /// - /// Looks up a localized string similar to Monitoring. - /// - public static string NavMenuMonitoringTab { + public static string MainLayoutAspire { get { - return ResourceManager.GetString("NavMenuMonitoringTab", resourceCulture); + return ResourceManager.GetString("MainLayoutAspire", resourceCulture); } } - /// - /// Looks up a localized string similar to Resources. - /// - public static string NavMenuResourcesTab { + public static string MessageTelemetryBody { get { - return ResourceManager.GetString("NavMenuResourcesTab", resourceCulture); + return ResourceManager.GetString("MessageTelemetryBody", resourceCulture); } } - /// - /// Looks up a localized string similar to Structured. - /// - public static string NavMenuStructuredLogsTab { + public static string MessageTelemetryLink { get { - return ResourceManager.GetString("NavMenuStructuredLogsTab", resourceCulture); + return ResourceManager.GetString("MessageTelemetryLink", resourceCulture); } } - /// - /// Looks up a localized string similar to Traces. - /// - public static string NavMenuTracesTab { + public static string MessageTelemetryTitle { get { - return ResourceManager.GetString("NavMenuTracesTab", resourceCulture); + return ResourceManager.GetString("MessageTelemetryTitle", resourceCulture); + } + } + + public static string PageLayoutViewFilters { + get { + return ResourceManager.GetString("PageLayoutViewFilters", resourceCulture); } } } diff --git a/src/Aspire.Dashboard/Resources/Layout.resx b/src/Aspire.Dashboard/Resources/Layout.resx index 970655bb58b..6334ad62be1 100644 --- a/src/Aspire.Dashboard/Resources/Layout.resx +++ b/src/Aspire.Dashboard/Resources/Layout.resx @@ -1,17 +1,17 @@  - @@ -124,7 +124,7 @@ Help - Launch settings + Settings An unhandled error has occurred. @@ -168,4 +168,7 @@ Telemetry endpoint is unsecured - \ No newline at end of file + + View Filters + + diff --git a/src/Aspire.Dashboard/Resources/TraceDetail.Designer.cs b/src/Aspire.Dashboard/Resources/TraceDetail.Designer.cs index d4cf97cda68..81936250d44 100644 --- a/src/Aspire.Dashboard/Resources/TraceDetail.Designer.cs +++ b/src/Aspire.Dashboard/Resources/TraceDetail.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -78,6 +77,15 @@ public static string TraceDetailDurationHeader { } } + /// + /// Looks up a localized string similar to Trace details. + /// + public static string TraceDetailMobileToolbarButtonText { + get { + return ResourceManager.GetString("TraceDetailMobileToolbarButtonText", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0} trace. /// diff --git a/src/Aspire.Dashboard/Resources/TraceDetail.resx b/src/Aspire.Dashboard/Resources/TraceDetail.resx index e32bfce696b..83d6fa0d07f 100644 --- a/src/Aspire.Dashboard/Resources/TraceDetail.resx +++ b/src/Aspire.Dashboard/Resources/TraceDetail.resx @@ -1,17 +1,17 @@  - @@ -140,4 +140,7 @@ Trace "{0}" not found {0} is an identifier - \ No newline at end of file + + Trace details + + diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.cs.xlf index db33b12f6f2..f03f21573af 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.cs.xlf @@ -18,8 +18,8 @@ - Launch settings - Nastavení pro spuštění + Settings + Nastavení pro spuštění @@ -87,6 +87,11 @@ Trasování + + View Filters + View Filters + + \ No newline at end of file diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.de.xlf index 3f4bc6f279d..759680a397b 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.de.xlf @@ -18,8 +18,8 @@ - Launch settings - Starteinstellungen + Settings + Starteinstellungen @@ -87,6 +87,11 @@ Überwachungen + + View Filters + View Filters + + \ No newline at end of file diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.es.xlf index f22f7b2bde1..508d18bc881 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.es.xlf @@ -18,8 +18,8 @@ - Launch settings - Configuración de inicio + Settings + Configuración de inicio @@ -87,6 +87,11 @@ Seguimientos + + View Filters + View Filters + + \ No newline at end of file diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.fr.xlf index 496ed54090d..a44645354b7 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.fr.xlf @@ -18,8 +18,8 @@ - Launch settings - Paramètres de lancement + Settings + Paramètres de lancement @@ -87,6 +87,11 @@ Traces + + View Filters + View Filters + + \ No newline at end of file diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.it.xlf index 03d5714ae7c..c5b4a65a4b1 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.it.xlf @@ -18,8 +18,8 @@ - Launch settings - Impostazioni di avvio + Settings + Impostazioni di avvio @@ -87,6 +87,11 @@ Tracce + + View Filters + View Filters + + \ No newline at end of file diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.ja.xlf index e1349af77ab..a4f75adf57a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.ja.xlf @@ -18,8 +18,8 @@ - Launch settings - 起動設定 + Settings + 起動設定 @@ -87,6 +87,11 @@ トレース + + View Filters + View Filters + + \ No newline at end of file diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.ko.xlf index 9c6de284551..8feadae7a3a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.ko.xlf @@ -18,8 +18,8 @@ - Launch settings - 시작 설정 + Settings + 시작 설정 @@ -87,6 +87,11 @@ 추적 + + View Filters + View Filters + + \ No newline at end of file diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.pl.xlf index 863f5124948..e3a0358a379 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.pl.xlf @@ -18,8 +18,8 @@ - Launch settings - Ustawienia uruchamiania + Settings + Ustawienia uruchamiania @@ -87,6 +87,11 @@ Ślady + + View Filters + View Filters + + \ No newline at end of file diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.pt-BR.xlf index 54c4f62e9ba..0f9e7407501 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.pt-BR.xlf @@ -18,8 +18,8 @@ - Launch settings - Configurações de inicialização + Settings + Configurações de inicialização @@ -87,6 +87,11 @@ Rastreamentos + + View Filters + View Filters + + \ No newline at end of file diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.ru.xlf index 8c5493d6ba7..e38dbb7fbd1 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.ru.xlf @@ -18,8 +18,8 @@ - Launch settings - Параметры запуска + Settings + Параметры запуска @@ -87,6 +87,11 @@ Трассировки + + View Filters + View Filters + + \ No newline at end of file diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.tr.xlf index 7fba438d534..7a08267ba78 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.tr.xlf @@ -18,8 +18,8 @@ - Launch settings - Başlatma ayarları + Settings + Başlatma ayarları @@ -87,6 +87,11 @@ İzlemeler + + View Filters + View Filters + + \ No newline at end of file diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hans.xlf index 9e99dfed574..a0569731430 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hans.xlf @@ -18,8 +18,8 @@ - Launch settings - 启动设置 + Settings + 启动设置 @@ -87,6 +87,11 @@ 跟踪 + + View Filters + View Filters + + \ No newline at end of file diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hant.xlf index bbf59fae1ed..47e681e2daf 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hant.xlf @@ -18,8 +18,8 @@ - Launch settings - 啟動設定 + Settings + 啟動設定 @@ -87,6 +87,11 @@ 追蹤 + + View Filters + View Filters + + \ No newline at end of file diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.cs.xlf index aece712f48e..c84580c3bdc 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.cs.xlf @@ -12,6 +12,11 @@ Doba trvání + + Trace details + Trace details + + {0} trace Trasa {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.de.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.de.xlf index 12569e21113..c279aa7aaf4 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.de.xlf @@ -12,6 +12,11 @@ Dauer + + Trace details + Trace details + + {0} trace {0}-Ablaufverfolgung diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.es.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.es.xlf index 7beb527dbbd..3c331025f7b 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.es.xlf @@ -12,6 +12,11 @@ Duración + + Trace details + Trace details + + {0} trace Seguimiento de {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.fr.xlf index 010184f38e5..516b7ddbae7 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.fr.xlf @@ -12,6 +12,11 @@ Durée + + Trace details + Trace details + + {0} trace Trace {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.it.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.it.xlf index 29a82efd30c..12bf106e1fa 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.it.xlf @@ -12,6 +12,11 @@ Durata + + Trace details + Trace details + + {0} trace Traccia {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ja.xlf index b64d1d976f1..2ae3a81934e 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ja.xlf @@ -12,6 +12,11 @@ 期間 + + Trace details + Trace details + + {0} trace {0} のトレース diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ko.xlf index 0ef602cc869..5523d0b2bf2 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ko.xlf @@ -12,6 +12,11 @@ 기간 + + Trace details + Trace details + + {0} trace {0} 추적 diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pl.xlf index 1a715dab392..acaf8019243 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pl.xlf @@ -12,6 +12,11 @@ Czas trwania + + Trace details + Trace details + + {0} trace Ślad {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pt-BR.xlf index eb6bc712457..bec75d25ede 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pt-BR.xlf @@ -12,6 +12,11 @@ Duração + + Trace details + Trace details + + {0} trace Rastreamento de{0} diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ru.xlf index 1dcfa02739b..fa1eb4170d2 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ru.xlf @@ -12,6 +12,11 @@ Длительность + + Trace details + Trace details + + {0} trace Трассировка {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.tr.xlf index dee61498427..f77111cbfb1 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.tr.xlf @@ -12,6 +12,11 @@ Süre + + Trace details + Trace details + + {0} trace {0} izleme diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hans.xlf index 0e582d85bdd..e123eb32884 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hans.xlf @@ -12,6 +12,11 @@ 持续时间 + + Trace details + Trace details + + {0} trace {0} 跟踪 diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hant.xlf index 5ff3e196b9d..8883c00140e 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hant.xlf @@ -12,6 +12,11 @@ 期間 + + Trace details + Trace details + + {0} trace {0} 追蹤 diff --git a/src/Aspire.Dashboard/wwwroot/css/app.css b/src/Aspire.Dashboard/wwwroot/css/app.css index 13c97796d7a..697471e1d74 100644 --- a/src/Aspire.Dashboard/wwwroot/css/app.css +++ b/src/Aspire.Dashboard/wwwroot/css/app.css @@ -98,33 +98,57 @@ h1 { margin-bottom: 0.5rem; } - #components-reconnect-modal :is(h5, p) { - color: var(--error-ui-foreground-color); - } +#components-reconnect-modal :is(h5, p) { + color: var(--error-ui-foreground-color); +} - #components-reconnect-modal a { - color: var(--error-ui-accent-foreground-color); - } +#components-reconnect-modal a { + color: var(--error-ui-accent-foreground-color); +} + +#components-reconnect-modal { + /* avoid making modal take the entire screen dimensions */ + inset: unset !important; + + /* force modal to be compact, centered, slightly padded, and towards the top - media queries below adjust the width and height of the modal for + mobile and desktop screens + */ + top: 5% !important; + padding: 2px; + + /* add a slight shadow */ + box-shadow: 2px 2px 2px var(--neutral-fill-secondary-rest); - #components-reconnect-modal { - /* avoid making modal take the entire screen dimensions */ - inset: unset !important; + /* avoid modal being see-through */ + opacity: 1 !important; - /* force modal to be compact, centered, slightly padded, and towards the top */ - top: 5% !important; - left: 30% !important; - right: 30% !important; - height: 105px; - padding: 2px; + /* ensure sufficient contrast between all elements in the modal and its background */ + background-color: var(--reconnection-ui-bg) !important; +} + +@media (max-width: 768px) { + #components-reconnect-modal { + left: 15% !important; + right: 15% !important; + height: 130px; + } - /* add a slight shadow */ - box-shadow: 2px 2px 2px var(--neutral-fill-secondary-rest); + /* we want less padding on mobile screens to preserve screen height for other elements */ + .page-header { + padding: calc(var(--design-unit) * 1.5px) calc(var(--design-unit) * 0.5px) 0; + } +} - /* avoid modal being see-through */ - opacity: 1 !important; + @media (min-width: 768px) { + #components-reconnect-modal { + left: 30% !important; + right: 30% !important; + height: 105px; + } - /* ensure sufficient contrast between all elements in the modal and its background */ - background-color: var(--reconnection-ui-bg) !important; + .page-header { + padding: calc(var(--design-unit) * 1.5px) calc(var(--design-unit) * 2.5px); + } } .datagrid-overflow-area, @@ -199,25 +223,6 @@ fluent-dialog fluent-data-grid-cell[cell-type=columnheader] fluent-button[appear background: var(--fill-layer); } -.content-layout-with-toolbar { - height: 100%; - width: 100%; - display: grid; - grid-template-rows: auto 1fr; - grid-template-areas: - "toolbar" - "main"; -} - -.content-layout-with-toolbar > fluent-toolbar { - grid-area: toolbar; -} - -.content-layout-with-toolbar > .datagrid-overflow-area, -.content-layout-with-toolbar > .parent-details-container { - grid-area: main; -} - .pane-details-subtext { color: var(--foreground-subtext-rest); padding-left: 0.5rem; @@ -273,10 +278,6 @@ fluent-data-grid-cell .long-inner-content { color: var(--error); } -.page-header { - padding: calc(var(--design-unit) * 1.5px) calc(var(--design-unit) * 2.5px); -} - .selected-row, .selected-row fluent-button[appearance=lightweight]:not(:hover)::part(control), .selected-row fluent-anchor[appearance=lightweight]:not(:hover)::part(control) { @@ -400,6 +401,7 @@ fluent-switch.table-switch::part(label) { } .total-items-footer { + padding-top: calc(var(--design-unit) * 2.5px); padding-left: calc(var(--design-unit) * 2.5px); padding-bottom: calc(var(--design-unit) * 2.5px); } @@ -438,3 +440,19 @@ fluent-data-grid-cell.no-ellipsis { .endpoint-button::part(control) { padding: calc(((var(--design-unit) * 0.5) - var(--stroke-width)) * 1px) calc((var(--design-unit) - var(--stroke-width)) * 1px); } + +.mobile-toolbar { + width: 100%; + height: max(5vh, 30px); +} + +.mobile-absolute-toolbar { + position: absolute; + left: 0; + bottom: 0; +} + +.page-content-container { + height: 100%; + overflow: auto; +} diff --git a/src/Aspire.Dashboard/wwwroot/js/app.js b/src/Aspire.Dashboard/wwwroot/js/app.js index 311a5ce9251..9ef506ebb50 100644 --- a/src/Aspire.Dashboard/wwwroot/js/app.js +++ b/src/Aspire.Dashboard/wwwroot/js/app.js @@ -266,3 +266,36 @@ window.focusElement = function(selector) { element.focus(); } } + +window.getWindowDimensions = function() { + return { + width: window.innerWidth, + height: window.innerHeight + } +} + +window.listenToWindowResize = function(dotnetHelper) { + function throttle(func, timeout) { + let currentTimeout = null; + return function () { + if (currentTimeout) { + return; + } + const context = this; + const args = arguments; + const later = () => { + func.call(context, ...args); + currentTimeout = null; + } + currentTimeout = setTimeout(later, timeout); + } + } + + const throttledResizeListener = throttle(() => { + dotnetHelper.invokeMethodAsync('OnResizeAsync', { width: window.innerWidth, height: window.innerHeight }); + }, 150) + + window.addEventListener('load', throttledResizeListener); + + window.addEventListener('resize', throttledResizeListener); +}