Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions playground/Stress/Stress.AppHost/InteractionCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ public static IResourceBuilder<T> AddInteractionCommands<T>(this IResourceBuilde
var numberOfPeopleInput = new InteractionInput { InputType = InputType.Number, Label = "Number of people", Placeholder = "Enter number of people", Value = "2", Required = true };
var inputs = new List<InteractionInput>
{
new InteractionInput { InputType = InputType.Text, Label = "Name", Placeholder = "Enter name", Required = true },
new InteractionInput { InputType = InputType.SecretText, Label = "Password", Placeholder = "Enter password", Required = true },
new InteractionInput { InputType = InputType.Text, Label = "Name", Placeholder = "Enter name", Required = true, MaxLength = 50 },
new InteractionInput { InputType = InputType.SecretText, Label = "Password", Placeholder = "Enter password", Required = true, MaxLength = 20 },
dinnerInput,
numberOfPeopleInput,
new InteractionInput { InputType = InputType.Boolean, Label = "Remember me", Placeholder = "What does this do?", Required = true },
Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Dashboard/Aspire.Dashboard.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,8 @@
<Compile Include="$(SharedDir)ConsoleLogs\LogEntry.cs" Link="Utils\ConsoleLogs\LogEntry.cs" />
<Compile Include="$(SharedDir)ConsoleLogs\LogPauseViewModel.cs" Link="Utils\ConsoleLogs\LogPauseViewModel.cs" />
<Compile Include="$(SharedDir)ConsoleLogs\TimestampParser.cs" Link="Utils\ConsoleLogs\TimestampParser.cs" />
<Compile Include="..\Shared\KnownUrls.cs" Link="Utils\KnownUrls.cs" />
<Compile Include="$(SharedDir)KnownUrls.cs" Link="Utils\KnownUrls.cs" />
<Compile Include="$(SharedDir)InteractionHelpers.cs" Link="Utils\InteractionHelpers.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@
<FluentStack Orientation="Orientation.Vertical" VerticalGap="12">
@foreach (var vm in _inputDialogInputViewModels)
{
@*
* AutoComplete value of one-time-code on password input prevents the browser asking to save the value.
* Immediate value of true on text inputs ensures the value is set to the server token with every key press in textbox.
*@
var localItem = vm;
var descriptionId = !string.IsNullOrEmpty(localItem.Input.Description)
? $"{_elementRefs[localItem]?.Id}-description"
Expand All @@ -45,17 +41,25 @@
@switch (vm.Input.InputType)
{
case InputType.Text:
@*
* Immediate value of true on text input ensures the value is set to the server token with every key press in textbox.
*@
<FluentTextField @ref="@_elementRefs[localItem]"
@bind-Value="localItem.Value"
Label="@localItem.Input.Label"
Placeholder="@localItem.Input.Placeholder"
Required="localItem.Input.Required"
Immediate="true"
aria-describedby="@descriptionId" />
aria-describedby="@descriptionId"
Maxlength="@InteractionHelpers.GetMaxLength(localItem.Input.MaxLength)" />
@GetDescriptionContent(localItem.Input, descriptionId)
<ValidationMessage For="@(() => localItem.Value)" />
break;
case InputType.SecretText:
@*
* AutoComplete value of one-time-code on password input prevents the browser asking to save the value.
* Immediate value of true on text input ensures the value is set to the server token with every key press in textbox.
*@
<FluentTextField @ref="@_elementRefs[localItem]"
@bind-Value="localItem.Value"
Label="@localItem.Input.Label"
Expand All @@ -64,7 +68,8 @@
TextFieldType="TextFieldType.Password"
AutoComplete="one-time-code"
Immediate="true"
aria-describedby="@descriptionId" />
aria-describedby="@descriptionId"
Maxlength="@InteractionHelpers.GetMaxLength(localItem.Input.MaxLength)" />
@GetDescriptionContent(localItem.Input, descriptionId)
<ValidationMessage For="@(() => localItem.Value)" />
break;
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Hosting/Aspire.Hosting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<Compile Include="$(SharedDir)PortAllocator.cs" Link="Publishing\PortAllocator.cs" />
<Compile Include="$(SharedDir)OverloadResolutionPriorityAttribute.cs" Link="Utils\OverloadResolutionPriorityAttribute.cs" />
<Compile Include="$(SharedDir)PackageUpdateHelpers.cs" Link="Utils\PackageUpdateHelpers.cs" />
<Compile Include="$(SharedDir)InteractionHelpers.cs" Link="Utils\InteractionHelpers.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 4 additions & 0 deletions src/Aspire.Hosting/Dashboard/DashboardService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ async Task WatchInteractionsInternal(CancellationToken cancellationToken)
{
dto.Options.Add(input.Options.ToDictionary());
}
if (input.MaxLength != null)
{
dto.MaxLength = input.MaxLength.Value;
}
dto.ValidationErrors.AddRange(input.ValidationErrors);
return dto;
}).ToList();
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ message InteractionInput {
repeated string validation_errors = 7;
string description = 8;
bool enable_description_markdown = 9;
int32 max_length = 10;
}
enum MessageIntent {
MESSAGE_INTENT_NONE = 0;
Expand Down
17 changes: 17 additions & 0 deletions src/Aspire.Hosting/IInteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,23 @@ public sealed class InteractionInput
/// </summary>
public string? Placeholder { get; set; }

/// <summary>
/// gets or sets the maximum length for text inputs.
/// </summary>
public int? MaxLength
{
get => field;
set
{
if (value is { } v)
{
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(v, 0);
}

field = value;
}
}

internal List<string> ValidationErrors { get; } = [];
}

Expand Down
77 changes: 65 additions & 12 deletions src/Aspire.Hosting/InteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Threading.Channels;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -234,25 +235,77 @@ private async Task<bool> RunValidationAsync(Interaction interactionState, Intera
// State could be null if the user dismissed the inputs dialog. There is nothing to validate in this situation.
if (result.State is IReadOnlyList<InteractionInput> inputs)
{
var options = (InputsDialogInteractionOptions)interactionState.Options;
foreach (var input in inputs)
{
input.ValidationErrors.Clear();
}

var context = new InputsDialogValidationContext
{
CancellationToken = cancellationToken,
ServiceProvider = _serviceProvider,
Inputs = inputsInfo.Inputs
};

if (options.ValidationCallback is { } validationCallback)
foreach (var input in inputs)
{
foreach (var input in inputs)
var value = input.Value = input.Value?.Trim();

if (string.IsNullOrEmpty(value))
{
input.ValidationErrors.Clear();
if (input.Required)
{
context.AddValidationError(input, "Value is required.");
}
}

var context = new InputsDialogValidationContext
else
{
CancellationToken = cancellationToken,
ServiceProvider = _serviceProvider,
Inputs = inputsInfo.Inputs
};
await validationCallback(context).ConfigureAwait(false);
switch (input.InputType)
{
case InputType.Text:
case InputType.SecretText:
var maxLength = InteractionHelpers.GetMaxLength(input.MaxLength);

if (value.Length > maxLength)
{
context.AddValidationError(input, $"Value length exceeds {maxLength} characters.");
}
break;
case InputType.Choice:
if (!input.Options?.Any(o => o.Key == value) ?? true)
{
context.AddValidationError(input, "Value must be one of the provided options.");
}
break;
case InputType.Boolean:
if (!bool.TryParse(value, out _))
{
context.AddValidationError(input, "Value must be a valid boolean.");
}
break;
case InputType.Number:
if (!int.TryParse(value, CultureInfo.InvariantCulture, out _))
{
context.AddValidationError(input, "Value must be a valid number.");
}
break;
default:
break;
}
}
}

return !context.HasErrors;
// Only run validation callback if there are no data validation errors.
if (!context.HasErrors)
{
var options = (InputsDialogInteractionOptions)interactionState.Options;
if (options.ValidationCallback is { } validationCallback)
{
await validationCallback(context).ConfigureAwait(false);
}
}

return !context.HasErrors;
}
}

Expand Down
27 changes: 27 additions & 0 deletions src/Shared/InteractionHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +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 System.Buffers;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;

namespace Aspire;

internal static class InteractionHelpers
{
// Chosen to balance between being long enough for most normal use but provides a default limit
// to prevent possible abuse of interactions API.
public const int DefaultMaxLength = 8000;

public static int GetMaxLength(int? configuredInputLength)
{
// An unconfigured max length uses the default.
if (configuredInputLength is null || configuredInputLength == 0)
{
return DefaultMaxLength;
}

return configuredInputLength.Value;
}
}
Loading
Loading