File: Components\Dialogs\TextVisualizerDialog.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 Aspire.Dashboard.Extensions;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Model.Markdown;
using Aspire.Dashboard.Utils;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.JSInterop;
 
namespace Aspire.Dashboard.Components.Dialogs;
 
public partial class TextVisualizerDialog : ComponentBase
{
    private readonly string _copyButtonId = $"copy-{Guid.NewGuid():N}";
    private readonly string _selectFormatId = $"select-format-{Guid.NewGuid():N}";
 
    private List<SelectViewModel<string>> _options = null!;
    private SelectViewModel<string> _selectedFormat = null!;
    private bool _isLoading = true;
    private MarkdownProcessor? _markdownProcessor;
    private TextVisualizerDialogViewModel? _previousContent;
    internal TextVisualizerViewModel TextVisualizerViewModel { get; set; } = default!;
 
    public HashSet<string?> EnabledOptions { get; } = [];
    internal bool? ShowSecretsWarning { get; private set; }
    internal bool IsMarkdownFormat => TextVisualizerViewModel.FormatKind == DashboardUIHelpers.MarkdownFormat;
 
    /// <summary>
    /// Returns true if the dialog has a fixed format that cannot be changed by the user.
    /// </summary>
    internal bool HasFixedFormat => Content.FixedFormat is not null;
 
    [Parameter, EditorRequired]
    public required TextVisualizerDialogViewModel Content { get; set; }
 
    [Inject]
    public required ILocalStorage LocalStorage { get; init; }
 
    [Inject]
    public required IJSRuntime JS { get; init; }
 
    protected override async Task OnInitializedAsync()
    {
        // We need to make users perform an explicit action once before being able to see secret values
        // We do this by making them agree to a warning in the text visualizer dialog.
        if (Content.ContainsSecret)
        {
            var settingsResult = await LocalStorage.GetUnprotectedAsync<TextVisualizerDialogSettings>(BrowserStorageKeys.TextVisualizerDialogSettings);
            ShowSecretsWarning = settingsResult.Value is not { SecretsWarningAcknowledged: true };
        }
 
        // Don't display content until it is loaded.
        // This is required because rendering uses the theme manager, and we don't want to call that code until we know it's finished initializing.
        _isLoading = false;
    }
 
    protected override void OnParametersSet()
    {
        _options = [
            new SelectViewModel<string> { Id = DashboardUIHelpers.PlaintextFormat, Name = Loc[nameof(Resources.Dialogs.TextVisualizerDialogPlaintextFormat)] },
            new SelectViewModel<string> { Id = DashboardUIHelpers.MarkdownFormat, Name = Loc[nameof(Resources.Dialogs.TextVisualizerDialogMarkdownFormat)] },
            new SelectViewModel<string> { Id = DashboardUIHelpers.JsonFormat, Name = Loc[nameof(Resources.Dialogs.TextVisualizerDialogJsonFormat)] },
            new SelectViewModel<string> { Id = DashboardUIHelpers.XmlFormat, Name = Loc[nameof(Resources.Dialogs.TextVisualizerDialogXmlFormat)] }
        ];
 
        if (_previousContent == Content)
        {
            _selectedFormat = GetSelectedFormatOption(TextVisualizerViewModel.FormatKind);
            return;
        }
 
        EnabledOptions.Clear();
        EnabledOptions.Add(DashboardUIHelpers.PlaintextFormat);
 
        // If a fixed format is specified, use it directly without auto-detection.
        if (Content.FixedFormat is not null)
        {
            TextVisualizerViewModel = new TextVisualizerViewModel(Content.Text, indentText: true, Content.FixedFormat);
        }
        else
        {
            TextVisualizerViewModel = new TextVisualizerViewModel(Content.Text, indentText: true);
 
            if (TextVisualizerViewModel.FormatKind == DashboardUIHelpers.JsonFormat)
            {
                EnabledOptions.Add(DashboardUIHelpers.JsonFormat);
            }
            else if (TextVisualizerViewModel.FormatKind == DashboardUIHelpers.XmlFormat)
            {
                EnabledOptions.Add(DashboardUIHelpers.XmlFormat);
            }
            else
            {
                // Markdown can't be reliably detected from content, so enable it when the format is
                // unknown to let users switch to markdown rendering if they want.
                EnabledOptions.Add(DashboardUIHelpers.MarkdownFormat);
            }
        }
 
        _previousContent = Content;
        _selectedFormat = GetSelectedFormatOption(TextVisualizerViewModel.FormatKind);
    }
 
    private bool IsTextContentDisplayed
    {
        get
        {
            if (_isLoading)
            {
                return false;
            }
 
            return !Content.ContainsSecret || ShowSecretsWarning is false;
        }
    }
 
    private void OnSelectedFormatChanged() => ChangeFormat(_selectedFormat.Id);
 
    internal void ChangeFormat(string? newFormat)
    {
        _selectedFormat = GetSelectedFormatOption(newFormat);
        TextVisualizerViewModel.UpdateFormat(_selectedFormat.Id ?? DashboardUIHelpers.PlaintextFormat);
    }
 
    private SelectViewModel<string> GetSelectedFormatOption(string? format) => _options.SingleOrDefault(o => o.Id == format) ?? _options[0];
 
    internal MarkdownProcessor GetMarkdownProcessor()
    {
        return _markdownProcessor ??= new MarkdownProcessor(ControlsStringsLoc, safeUrlSchemes: MarkdownHelpers.SafeUrlSchemes, extensions: []);
    }
 
    public static async Task<IDialogReference> OpenDialogAsync(OpenTextVisualizerDialogOptions options)
    {
        var width = options.DialogService.IsDesktop ? "75vw" : "100vw";
        var parameters = new DialogParameters
        {
            Title = options.ValueDescription,
            Width = $"min(1000px, {width})",
            TrapFocus = true,
            Modal = true,
            PreventScroll = true,
        };
 
        return await options.DialogService.ShowDialogAsync<TextVisualizerDialog>(
            new TextVisualizerDialogViewModel(options.Value, options.ValueDescription, options.ContainsSecret, options.DownloadFileName, options.FixedFormat), parameters);
    }
 
    private async Task DownloadAsync()
    {
        if (Content.DownloadFileName is not null)
        {
            await JS.DownloadFileAsync(Content.DownloadFileName, TextVisualizerViewModel.FormattedText);
        }
    }
 
    internal sealed record TextVisualizerDialogSettings(bool SecretsWarningAcknowledged);
 
    private async Task UnmaskContentAsync()
    {
        await LocalStorage.SetUnprotectedAsync(BrowserStorageKeys.TextVisualizerDialogSettings, new TextVisualizerDialogSettings(SecretsWarningAcknowledged: true));
        ShowSecretsWarning = false;
    }
}