| File: Components\Controls\SummaryDetailsView.razor.cs | Web Access |
| Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; using Aspire.Dashboard.Model; using Aspire.Dashboard.Utils; using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.JSInterop; namespace Aspire.Dashboard.Components.Controls; public sealed record SummaryDetailsSize(Orientation Orientation, float Panel1Fraction); public partial class SummaryDetailsView<T> : IGlobalKeydownListener, IDisposable { [Parameter] public RenderFragment? Summary { get; set; } [Parameter] public RenderFragment<T>? Details { get; set; } [Parameter] public bool ShowDetails { get; set; } [Parameter] public string? DetailsTitle { get; set; } [Parameter] public Orientation Orientation { get; set; } = Orientation.Horizontal; [Parameter] public EventCallback OnDismiss { get; set; } [Parameter] public EventCallback<SummaryDetailsSize> OnResize { get; set; } [Parameter] public bool RememberSize { get; set; } = true; [Parameter] public bool RememberOrientation { get; set; } = true; [Parameter, EditorRequired] public T? SelectedValue { get; set; } /// <summary> /// Overrides the default key used to store the splitter size and orientation in local storage. /// By default, the key is based on the current URL. If you have multiple instances of this control /// on a page or want to share the same settings across multiple pages, you can set this property /// </summary> [Parameter] public string? ViewKey { get; set; } [Parameter] public RenderFragment<T>? DetailsTitleTemplate { get; set; } [Inject] public required ILocalStorage LocalStore { get; init; } [Inject] public required NavigationManager NavigationManager { get; init; } [Inject] public required IJSRuntime JS { get; init; } [Inject] public required ShortcutManager ShortcutManager { get; init; } [CascadingParameter] public required ViewportInformation ViewportInformation { get; set; } private float _panel1Fraction; private string _panel1Size { get; set; } = "1fr"; private string _panel2Size { get; set; } = "1fr"; private bool _internalShowDetails; private FluentSplitter? _splitterRef; public string EffectivePanel1Size => ViewportInformation.IsDesktop ? _panel1Size : "0fr"; public string EffectivePanel2Size => ViewportInformation.IsDesktop ? _panel2Size : "1fr"; public string PanelMinimumSize => ViewportInformation.IsDesktop ? "150px" : "0"; protected override void OnInitialized() { ResetPanelSizes(); } protected override void OnAfterRender(bool firstRender) { if (firstRender) { ShortcutManager.AddGlobalKeydownListener(this); } } protected override async Task OnParametersSetAsync() { // Is visible state changing? if (ShowDetails != _internalShowDetails) { // If show details is changing from false to true, read saved state. if (ShowDetails) { if (RememberOrientation) { var orientationResult = await LocalStore.GetUnprotectedAsync<Orientation>(GetOrientationStorageKey()); if (orientationResult.Success) { Orientation = orientationResult.Value; } } if (RememberSize) { var panel1FractionResult = await LocalStore.GetUnprotectedAsync<float>(GetSizeStorageKey()); if (panel1FractionResult.Success) { var fraction = Math.Clamp(panel1FractionResult.Value, 0, 1); SetPanelSizes(fraction); } } } await RaiseOnResizeAsync(); } // Bind visibility to internal bool that is set after reading from local store. // 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; } private async Task RaiseOnResizeAsync() { await OnResize.InvokeAsync(new SummaryDetailsSize(Orientation, ShowDetails ? _panel1Fraction : 1)); } private async Task HandleDismissAsync() { await OnDismiss.InvokeAsync(); } private async Task HandleToggleOrientation() { if (Orientation == Orientation.Horizontal) { Orientation = Orientation.Vertical; } else { Orientation = Orientation.Horizontal; } if (RememberOrientation) { await LocalStore.SetUnprotectedAsync(GetOrientationStorageKey(), Orientation); } if (RememberSize) { var panel1FractionResult = await LocalStore.GetUnprotectedAsync<float>(GetSizeStorageKey()); if (panel1FractionResult.Success) { var fraction = Math.Clamp(panel1FractionResult.Value, 0, 1); SetPanelSizes(fraction); } else { ResetPanelSizes(); } } else { ResetPanelSizes(); } await RaiseOnResizeAsync(); // The FluentSplitter control will render during the async calls above, but with the wrong values. // We need to force a re-render to get the correct values. StateHasChanged(); } private async Task HandleSplitterResize(SplitterResizedEventArgs args) { var totalSize = (float)(args.Panel1Size + args.Panel2Size); var panel1Fraction = (args.Panel1Size / totalSize); SetPanelSizes(panel1Fraction); if (RememberSize) { await SaveSizeToStorage(panel1Fraction); } await RaiseOnResizeAsync(); } private async Task SaveSizeToStorage(float panel1Fraction) { await LocalStore.SetUnprotectedAsync(GetSizeStorageKey(), Math.Round(panel1Fraction, 3)); } private void ResetPanelSizes() { _panel1Fraction = 0.5f; _panel1Size = "0.5fr"; _panel2Size = "0.5fr"; } private void SetPanelSizes(float panel1Fraction) { _panel1Fraction = panel1Fraction; // These need to not use culture-specific formatting because it needs to be a valid CSS value _panel1Size = string.Create(CultureInfo.InvariantCulture, $"{panel1Fraction:F3}fr"); _panel2Size = string.Create(CultureInfo.InvariantCulture, $"{(1 - panel1Fraction):F3}fr"); } public IReadOnlySet<AspireKeyboardShortcut> SubscribedShortcuts { get; } = new HashSet<AspireKeyboardShortcut> { AspireKeyboardShortcut.ToggleOrientation, AspireKeyboardShortcut.ClosePanel, AspireKeyboardShortcut.ResetPanelSize, AspireKeyboardShortcut.IncreasePanelSize, AspireKeyboardShortcut.DecreasePanelSize }; public async Task OnPageKeyDownAsync(AspireKeyboardShortcut shortcut) { if (_splitterRef is null) { return; } if (shortcut is AspireKeyboardShortcut.ToggleOrientation) { await HandleToggleOrientation(); return; } if (shortcut is AspireKeyboardShortcut.ClosePanel) { if (SelectedValue is not null) { await HandleDismissAsync(); await InvokeAsync(StateHasChanged); } return; } var hasChanged = false; if (shortcut is AspireKeyboardShortcut.ResetPanelSize) { ResetPanelSizes(); hasChanged = true; } GetPanelSizes(_splitterRef.Panel1Size, _splitterRef.Panel2Size, out var panel1Size, out var panel2Size, out var panel1Fraction); if (panel1Size is null || panel2Size is null || panel1Fraction is null) { return; } if (shortcut is AspireKeyboardShortcut.IncreasePanelSize) { SetPanelSizes(panel1Fraction.Value - 0.05f); hasChanged = true; } else if (shortcut is AspireKeyboardShortcut.DecreasePanelSize) { SetPanelSizes(panel1Fraction.Value + 0.05f); hasChanged = true; } GetPanelSizes(_splitterRef.Panel1Size, _splitterRef.Panel2Size, out _, out _, out var newPanel1Fraction); if (newPanel1Fraction is null || !hasChanged) { return; } await SaveSizeToStorage(newPanel1Fraction.Value); await InvokeAsync(async () => { await RaiseOnResizeAsync(); StateHasChanged(); }); return; static void GetPanelSizes( string? panel1SizeString, string? panel2SizeString, out float? panel1Size, out float? panel2Size, out float? panel1Fraction) { if (panel1SizeString is null || !panel1SizeString.EndsWith("fr") || panel2SizeString is null || !panel2SizeString.EndsWith("fr")) { panel1Size = null; panel2Size = null; panel1Fraction = null; return; } panel1Size = (float)Convert.ToDouble(panel1SizeString[..^2], CultureInfo.InvariantCulture); panel2Size = (float)Convert.ToDouble(panel2SizeString[..^2], CultureInfo.InvariantCulture); var newTotalSize = (float)(panel1Size + panel2Size); panel1Fraction = panel1Size.Value / newTotalSize; } } private string GetSizeStorageKey() { var viewKey = ViewKey ?? NavigationManager.ToBaseRelativePath(NavigationManager.Uri); return BrowserStorageKeys.SplitterSizeKey(viewKey, Orientation); } private string GetOrientationStorageKey() { var viewKey = ViewKey ?? NavigationManager.ToBaseRelativePath(NavigationManager.Uri); return BrowserStorageKeys.SplitterOrientationKey(viewKey); } public void Dispose() { ShortcutManager.RemoveGlobalKeydownListener(this); } }