| File: Components\Controls\MissingParametersPanel.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.Model; using Aspire.Dashboard.Model.Interaction; using Aspire.Dashboard.Model.Markdown; using Aspire.Dashboard.Resources; using Aspire.Dashboard.Utils; using Aspire.DashboardService.Proto.V1; using Google.Protobuf.WellKnownTypes; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; using Microsoft.Extensions.Localization; namespace Aspire.Dashboard.Components.Controls; public partial class MissingParametersPanel : IAsyncDisposable { private const string SaveToUserSecretsName = ParameterInteractionState.SaveToUserSecretsInputName; private readonly CancellationTokenSource _disposalCts = new(); private ParameterInteractionState.PanelRegistration? _registration; private IReadOnlyList<ParameterEditor> _editors = []; private InputViewModel? _saveInput; private bool _saveToUserSecrets; [Inject] public required IStringLocalizer<Resources.Resources> Loc { get; init; } [Inject] public required IStringLocalizer<ControlsStrings> ControlsStringsLoc { get; init; } [Inject] public required IStringLocalizer<Resources.Dialogs> DialogsLoc { get; init; } [Inject] public required ParameterInteractionState ParameterInteractionState { get; init; } [Parameter, EditorRequired] public required IReadOnlyList<ResourceViewModel> Parameters { get; set; } [Parameter, EditorRequired] public required Func<ResourceViewModel, CommandViewModel, Task> OnExecuteCommandAsync { get; set; } [Parameter, EditorRequired] public required Func<ResourceViewModel, CommandViewModel, ExecuteResourceCommandOptions, Task> OnExecuteCommandWithOptionsAsync { get; set; } [Parameter, EditorRequired] public required Func<ResourceViewModel, CommandViewModel, bool> IsCommandExecuting { get; set; } private IReadOnlyList<ParameterEditor> Editors => _editors; private MarkdownProcessor MarkdownProcessor { get; set; } = null!; // With the AppHost's interaction, the choice is the interaction's own input. Without it, the choice is passed to // each parameter's command. private bool SaveToUserSecrets { get => _saveInput?.IsChecked ?? _saveToUserSecrets; set { if (_saveInput is not null) { _saveInput.IsChecked = value; } else { _saveToUserSecrets = value; } } } private bool IsSubmitDisabled => Editors.Count == 0 || Editors.Any(editor => IsEditorDisabled(editor) || IsMissingRequiredValue(editor.ValueInput)); // The AppHost's inputs interaction, when it's shown in this panel. The interaction is submitted as a whole, so the // panel uses it only when it shows every parameter the interaction asks for. Otherwise each parameter is set with // its own command. private InteractionsInputsDialogViewModel? Interaction => ParameterInteractionState.Current is { } current && _registration?.Shows(current) == true ? current : null; protected override void OnInitialized() { MarkdownProcessor = InteractionMarkdownHelper.CreateProcessor(ControlsStringsLoc); _registration = ParameterInteractionState.RegisterPanel(); ParameterInteractionState.Changed += OnParameterInteractionChanged; } protected override async Task OnParametersSetAsync() { // The registered parameters decide whether the interaction is shown here, so update them before the editors. _registration!.SetParameterNames(Parameters .Where(parameter => GetSetParameterCommand(parameter) is not null) .Select(parameter => parameter.Name)); SyncEditors(); await ParameterInteractionState.BeginInteractionAsync(_registration); } private static CommandViewModel? GetSetParameterCommand(ResourceViewModel parameter) => parameter.Commands.FirstOrDefault(command => string.Equals(command.Name, CommandViewModel.SetParameterCommand, StringComparisons.CommandName) && command.State != CommandViewModelState.Hidden); private void SyncEditors() { var existingEditors = _editors.ToDictionary(editor => editor.Resource.Name, StringComparers.ResourceName); var editors = new List<ParameterEditor>(); var interaction = Interaction; var interactionInputs = interaction?.Inputs .ToDictionary(input => input.Name, StringComparers.InteractionInputName); foreach (var parameter in Parameters .Where(parameter => parameter.HasMissingParameterValueState()) .OrderBy(parameter => parameter.DisplayName, StringComparers.ResourceName)) { var command = GetSetParameterCommand(parameter); var valueInput = interactionInputs?.GetValueOrDefault(parameter.Name) ?? command?.ArgumentInputs.FirstOrDefault(input => !string.Equals(input.Name, SaveToUserSecretsName, StringComparisons.InteractionInputName)); if (command is null || valueInput is null) { continue; } var saveInput = command.ArgumentInputs.FirstOrDefault(input => string.Equals(input.Name, SaveToUserSecretsName, StringComparisons.InteractionInputName)); if (existingEditors.TryGetValue(parameter.Name, out var existingEditor)) { existingEditor.ValueInput.SetInput(valueInput); editors.Add(existingEditor with { Resource = parameter, Command = command, SaveInput = saveInput }); } else { editors.Add(new ParameterEditor(parameter, command, new InputViewModel(valueInput), saveInput)); } } _editors = editors; var interactionSaveInput = interaction?.Inputs.FirstOrDefault(input => string.Equals(input.Name, SaveToUserSecretsName, StringComparisons.InteractionInputName)); if (interactionSaveInput is not null) { _saveInput ??= new InputViewModel(interactionSaveInput); _saveInput.SetInput(interactionSaveInput); } else { // The interaction completed, or moved to a dialog. _saveInput = null; } } private bool IsEditorDisabled(ParameterEditor editor) => editor.ValueInput.InputDisabled || editor.Command.State == CommandViewModelState.Disabled || IsCommandExecuting(editor.Resource, editor.Command); private static bool IsMissingRequiredValue(InputViewModel input) => input.Input.Required && input.Input.InputType != InputType.Boolean && string.IsNullOrWhiteSpace(input.Value); private async Task SubmitAsync() { if (IsSubmitDisabled) { return; } if (Interaction is { } interaction) { await interaction.OnSubmitCallback(interaction.Interaction, false); return; } foreach (var editor in Editors.Where(editor => editor.ValueInput.Input.InputType != InputType.File)) { var arguments = new Dictionary<string, Value> { [editor.ValueInput.Input.Name] = GetArgumentValue(editor.ValueInput) }; if (editor.SaveInput is { } saveInput) { arguments[saveInput.Name] = Value.ForBool(SaveToUserSecrets); } await OnExecuteCommandWithOptionsAsync(editor.Resource, editor.Command, new ExecuteResourceCommandOptions { Arguments = arguments, NonInteractive = true }); } } private static long GetMaxFileSize(InputViewModel input) => input.Input.MaxFileSize > 0 ? input.Input.MaxFileSize : InputViewModel.DefaultMaxUploadedFileBytes; private async Task OnInputFileChangeAsync(ParameterEditor editor, InputFileChangeEventArgs args) { if (Interaction is not { } interaction) { return; } var input = editor.ValueInput; var maxFileSize = GetMaxFileSize(input); var fileReferences = new List<FileReferenceViewModel>(); foreach (var file in args.GetMultipleFiles(InteractionHelpers.MaxFileCount)) { if (file.Size > maxFileSize) { fileReferences.Add(new FileReferenceViewModel { Name = file.Name, ErrorMessage = string.Format( CultureInfo.CurrentCulture, DialogsLoc[nameof(Resources.Dialogs.InteractionFileExceedsMaxSize)], FormatHelpers.FormatFileSize(maxFileSize)) }); continue; } try { using var stream = file.OpenReadStream(maxFileSize); var fileId = await interaction.DashboardClient.UploadFileAsync( stream, file.Name, file.Size, interaction.Interaction.InteractionId, input.Input.Name, _disposalCts.Token); fileReferences.Add(new FileReferenceViewModel { Id = fileId, Name = file.Name }); } catch (Exception ex) when (ex is not OperationCanceledException) { fileReferences.Add(new FileReferenceViewModel { Name = file.Name, ErrorMessage = DialogsLoc[nameof(Resources.Dialogs.InteractionFileUploadFailed)] }); } } input.SetFileReferences(fileReferences); } private static Value GetArgumentValue(InputViewModel input) => input.Input.InputType switch { InputType.Boolean => Value.ForBool(input.IsChecked), InputType.Number when input.NumberValue is { } number => Value.ForNumber(number), _ => Value.ForString(input.Value) }; private static void OnChoiceTextChanged(InputViewModel input, string? text) { if (input.SelectedOption?.Name == text) { return; } text ??= string.Empty; input.SelectedOption = input.SelectOptions.FirstOrDefault(option => option.Name == text) ?? new SelectViewModel<string> { Id = text, Name = text }; UpdateChoiceValueState(input); } private static void UpdateChoiceValueState(InputViewModel input) { input.SelectedOption ??= input.SelectOptions.FirstOrDefault(option => option.Id == input.Value) ?? new SelectViewModel<string> { Id = input.Value, Name = input.Value }; input.Value = input.SelectedOption.Id; } private static string GetFileButtonText(ParameterEditor editor) => string.IsNullOrEmpty(editor.ValueInput.Input.Placeholder) ? editor.Command.GetDisplayName() : editor.ValueInput.Input.Placeholder; private void OnParameterInteractionChanged() { _ = InvokeAsync(async () => { SyncEditors(); StateHasChanged(); await ParameterInteractionState.BeginInteractionAsync(_registration!); }); } public ValueTask DisposeAsync() { ParameterInteractionState.Changed -= OnParameterInteractionChanged; _registration?.Dispose(); _disposalCts.Cancel(); _disposalCts.Dispose(); return ValueTask.CompletedTask; } private sealed record ParameterEditor( ResourceViewModel Resource, CommandViewModel Command, InputViewModel ValueInput, InteractionInput? SaveInput); }