| File: Components\Controls\TerminalTitle.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.Extensions; using Aspire.Dashboard.Utils; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Localization; using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.JSInterop; namespace Aspire.Dashboard.Components.Controls; /// <summary>Displays the workload title, directory and progress without changing terminal state.</summary> public partial class TerminalTitle : IAsyncDisposable { private static readonly Icon s_terminalIcon = new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size20.WindowConsole(); private readonly string _titleButtonId = $"terminal-title-{Guid.NewGuid():N}"; private readonly string _directoryButtonId = $"terminal-directory-{Guid.NewGuid():N}"; private ElementReference _metadataElement; private IJSObjectReference? _jsModule; private Task? _initializationTask; private bool _disposed; /// <summary>Gets or sets the terminal's latest presentation state.</summary> [Parameter] public TerminalToolbarState? State { get; set; } /// <summary>Gets or sets the title used before the workload reports one or after it clears it.</summary> [Parameter] public string FallbackTitle { get; set; } = string.Empty; /// <summary>Gets or sets the resource icon shown when progress is inactive; defaults to the terminal icon.</summary> [Parameter] public Icon? IdleIcon { get; set; } [Inject] public required IStringLocalizer<Resources.TerminalStrings> Loc { get; init; } [Inject] public required IStringLocalizer<Resources.ControlsStrings> ControlsLoc { get; init; } [Inject] public required IJSRuntime JS { get; init; } [Inject] public required ILogger<TerminalTitle> Logger { get; init; } protected override Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { _initializationTask = InitializeAsync(); } return _initializationTask ?? Task.CompletedTask; } private async Task InitializeAsync() { try { _jsModule = await JS.InvokeAsync<IJSObjectReference>("import", $"./{Assets["Components/Controls/TerminalTitle.razor.js"]}"); if (!_disposed) { await _jsModule.InvokeVoidAsync("observePath", _metadataElement); } } catch (JSDisconnectedException) { // The circuit is gone; there is no metadata observer to keep alive. } catch (OperationCanceledException) when (_disposed) { // Disposal can interrupt the optional observer setup. } catch (JSException ex) { Logger.LogWarning(ex, "Could not initialize terminal path measurement."); } } /// <summary>Releases the path measurement observers.</summary> public async ValueTask DisposeAsync() { _disposed = true; try { if (_initializationTask is not null) { await _initializationTask; } if (_jsModule is not null) { await _jsModule.InvokeVoidAsync("disconnectPath", _metadataElement); } } catch (JSDisconnectedException) { // The browser may already be gone when the component is disposed. } catch (OperationCanceledException) { // The browser may already be gone when the component is disposed. } finally { if (_jsModule is not null) { await JSInteropHelpers.SafeDisposeAsync(_jsModule); } } } private Dictionary<string, object> TitleCopyAttributes => GetCopyAttributes(DisplayTitle, nameof(Resources.TerminalStrings.TerminalCopyTitle)); private Dictionary<string, object> DirectoryCopyAttributes => GetCopyAttributes(State?.WorkingDirectory ?? string.Empty, nameof(Resources.TerminalStrings.TerminalCopyWorkingDirectory)); private Dictionary<string, object> GetCopyAttributes(string value, string labelResourceName) => FluentUIExtensions.GetClipboardCopyAdditionalAttributes( value, ControlsLoc[nameof(Resources.ControlsStrings.GridValueCopyToClipboard)], ControlsLoc[nameof(Resources.ControlsStrings.GridValueCopied)], ("data-copyfailed", Loc[nameof(Resources.TerminalStrings.TerminalCopyFailed)].Value), ("aria-label", Loc[labelResourceName, value].Value)); private string DisplayTitle => State is { Title.Length: > 0 } ? State.Title : FallbackTitle; private double? ProgressValue => State?.ProgressState is "indeterminate" ? null : State?.ProgressPercentage; private bool HasProgress => State is { Connected: true, ProgressState: "normal" or "indeterminate" or "error" or "warning" }; private string ProgressTooltip => ProgressValue is { } percentage ? (percentage / 100).ToString("P0", CultureInfo.CurrentCulture) : ProgressLabel; private string ProgressLabel => Loc[State?.ProgressState switch { "error" => nameof(Resources.TerminalStrings.TerminalProgressError), "warning" => nameof(Resources.TerminalStrings.TerminalProgressWarning), _ => nameof(Resources.TerminalStrings.TerminalProgress) }]; }