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
121 changes: 120 additions & 1 deletion playground/Stress/Stress.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;

#pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

var builder = DistributedApplication.CreateBuilder(args);
builder.Services.AddHttpClient();
Expand Down Expand Up @@ -113,7 +116,121 @@
await ExecuteCommandForAllResourcesAsync(c.ServiceProvider, "resource-start", c.CancellationToken);
return CommandResults.Success();
},
commandOptions: new() { IconName = "Play", IconVariant = IconVariant.Filled });
commandOptions: new() { IconName = "Play", IconVariant = IconVariant.Filled })
.WithCommand("confirmation-interaction", "Confirmation interactions", executeCommand: async commandContext =>
{
var interactionService = commandContext.ServiceProvider.GetRequiredService<InteractionService>();
var resultTask1 = interactionService.PromptConfirmationAsync("Command confirmation", "Are you sure?", cancellationToken: commandContext.CancellationToken);
var resultTask2 = interactionService.PromptConfirmationAsync("Command confirmation", "Are you really sure?", new MessageBoxInteractionOptions { Intent = MessageIntent.Warning }, cancellationToken: commandContext.CancellationToken);

await Task.WhenAll(resultTask1, resultTask2);

if (resultTask1.Result.Data != true || resultTask2.Result.Data != true)
{
return CommandResults.Failure("Canceled");
}

_ = interactionService.PromptMessageBoxAsync("Command executed", "The command successfully executed.", new MessageBoxInteractionOptions { Intent = MessageIntent.Success, PrimaryButtonText = "Yeah!" });
return CommandResults.Success();
})
.WithCommand("messagebar-interaction", "Messagebar interactions", executeCommand: async commandContext =>
{
await Task.Yield();

var interactionService = commandContext.ServiceProvider.GetRequiredService<InteractionService>();
_ = interactionService.PromptMessageBarAsync("Success bar", "The command successfully executed.", new MessageBoxInteractionOptions { Intent = MessageIntent.Success });
_ = interactionService.PromptMessageBarAsync("Information bar", "The command successfully executed.", new MessageBoxInteractionOptions { Intent = MessageIntent.Information });
_ = interactionService.PromptMessageBarAsync("Warning bar", "The command successfully executed.", new MessageBoxInteractionOptions { Intent = MessageIntent.Warning });
_ = interactionService.PromptMessageBarAsync("Error bar", "The command successfully executed.", new MessageBoxInteractionOptions { Intent = MessageIntent.Error });
_ = interactionService.PromptMessageBarAsync("Confirmation bar", "The command successfully executed.", new MessageBoxInteractionOptions { Intent = MessageIntent.Confirmation });
_ = interactionService.PromptMessageBarAsync("No dismiss", "The command successfully executed.", new MessageBoxInteractionOptions { Intent = MessageIntent.Information, ShowDismiss = false });

return CommandResults.Success();
})
.WithCommand("html-interaction", "HTML interactions", executeCommand: async commandContext =>
{
var interactionService = commandContext.ServiceProvider.GetRequiredService<InteractionService>();

_ = interactionService.PromptMessageBarAsync("Success <strong>bar</strong>", "The <strong>command</strong> successfully executed.", new MessageBoxInteractionOptions { Intent = MessageIntent.Success });
_ = interactionService.PromptMessageBarAsync("Success <strong>bar</strong>", "The <strong>command</strong> successfully executed.", new MessageBoxInteractionOptions { Intent = MessageIntent.Success, EscapeMessageHtml = false });

_ = interactionService.PromptMessageBoxAsync("Success <strong>bar</strong>", "The <strong>command</strong> successfully executed.", new MessageBoxInteractionOptions { Intent = MessageIntent.Success });
_ = interactionService.PromptMessageBoxAsync("Success <strong>bar</strong>", "The <strong>command</strong> successfully executed.", new MessageBoxInteractionOptions { Intent = MessageIntent.Success, EscapeMessageHtml = false });

_ = await interactionService.PromptInputAsync("Text <strong>request</strong>", "Provide <strong>your</strong> name", "<strong>Name</strong>", "Enter <strong>your</strong> name");
_ = await interactionService.PromptInputAsync("Text <strong>request</strong>", "Provide <strong>your</strong> name", "<strong>Name</strong>", "Enter <strong>your</strong> name", new InputsDialogInteractionOptions { EscapeMessageHtml = false });

return CommandResults.Success();
})
.WithCommand("value-interaction", "Value interactions", executeCommand: async commandContext =>
{
var interactionService = commandContext.ServiceProvider.GetRequiredService<InteractionService>();
var result = await interactionService.PromptInputAsync(
title: "Text request",
message: "Provide your name",
inputLabel: "Name",
placeHolder: "Enter your name",
cancellationToken: commandContext.CancellationToken);

if (result.Canceled)
{
return CommandResults.Failure("Canceled");
}

var resourceLoggerService = commandContext.ServiceProvider.GetRequiredService<ResourceLoggerService>();
var logger = resourceLoggerService.GetLogger(commandContext.ResourceName);

var input = result.Data!;
logger.LogInformation("Input: {Label} = {Value}", input.Label, input.Value);

return CommandResults.Success();
})
.WithCommand("input-interaction", "Input interactions", executeCommand: async commandContext =>
{
var interactionService = commandContext.ServiceProvider.GetRequiredService<InteractionService>();
var inputs = new List<InteractionInput>
{
new InteractionInput { InputType = InputType.Text, Label = "Name", Placeholder = "Enter name", Required = true },
new InteractionInput { InputType = InputType.Password, Label = "Password", Placeholder = "Enter password", Required = true },
new InteractionInput { InputType = InputType.Select, Label = "Dinner", Placeholder = "Select dinner", Required = true, Options =
[
KeyValuePair.Create("pizza", "Pizza"),
KeyValuePair.Create("fried-chicken", "Fried chicken"),
KeyValuePair.Create("burger", "Burger"),
KeyValuePair.Create("salmon", "Salmon"),
KeyValuePair.Create("chicken-pie", "Chicken pie"),
KeyValuePair.Create("sushi", "Sushi"),
KeyValuePair.Create("tacos", "Tacos"),
KeyValuePair.Create("pasta", "Pasta"),
KeyValuePair.Create("salad", "Salad"),
KeyValuePair.Create("steak", "Steak"),
KeyValuePair.Create("vegetarian", "Vegetarian"),
KeyValuePair.Create("sausage", "Sausage"),
KeyValuePair.Create("lasagne", "Lasagne"),
KeyValuePair.Create("fish-pie", "Fish pie"),
KeyValuePair.Create("soup", "Soup"),
KeyValuePair.Create("beef-stew", "Beef stew"),
] },
new InteractionInput { InputType = InputType.Number, Label = "Number of people", Placeholder = "Enter number of people", Value = "2", Required = true },
new InteractionInput { InputType = InputType.Checkbox, Label = "Remember me", Placeholder = "What does this do?", Required = true },
};
var result = await interactionService.PromptInputsAsync("Input request", "Provide your name", inputs, cancellationToken: commandContext.CancellationToken);

if (result.Canceled)
{
return CommandResults.Failure("Canceled");
}

var resourceLoggerService = commandContext.ServiceProvider.GetRequiredService<ResourceLoggerService>();
var logger = resourceLoggerService.GetLogger(commandContext.ResourceName);

foreach (var updatedInput in result.Data!)
{
logger.LogInformation("Input: {Label} = {Value}", updatedInput.Label, updatedInput.Value);
}

return CommandResults.Success();
});

#if !SKIP_DASHBOARD_REFERENCE
// This project is only added in playground projects to support development/debugging
Expand Down Expand Up @@ -158,3 +275,5 @@ static async Task ExecuteCommandForAllResourcesAsync(IServiceProvider servicePro
}
await Task.WhenAll(commandTasks).ConfigureAwait(false);
}

#pragma warning restore ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ private static async Task ExportMetrics(ILogger<TelemetryStresser> logger, Metad
}
};

logger.LogDebug("Exporting metrics");
var response = await client.ExportAsync(request, headers: metadata, cancellationToken: cancellationToken);
logger.LogDebug($"Export complete. Rejected count: {response.PartialSuccess?.RejectedDataPoints ?? 0}");
if (response.PartialSuccess is { RejectedDataPoints: > 0 } result)
{
logger.LogDebug($"Export complete. Rejected count: {result.RejectedDataPoints}");
}
}

public static Resource CreateResource(string? name = null, string? instanceId = null)
Expand Down
2 changes: 1 addition & 1 deletion playground/TestShop/TestShop.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
.WithHttpHealthCheck("/health");

builder.AddProject<Projects.OrderProcessor>("orderprocessor", launchProfileName: "OrderProcessor")
.WithReference(messaging).WaitFor(messaging);
.WithReference(messaging).WaitFor(messaging);

builder.AddYarp("apigateway")
.WithConfigFile("yarp.json")
Expand Down
1 change: 0 additions & 1 deletion src/Aspire.Dashboard/Aspire.Dashboard.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
</Protobuf>

<Protobuf Include="..\Aspire.Hosting\Dashboard\proto\resource_service.proto">
<Access>Internal</Access>
<Link>ResourceService\resource_service.proto</Link>
</Protobuf>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
@using Aspire.Dashboard.Components.Controls.Chart
@using Aspire.Dashboard.Model
@using Aspire.Dashboard.Model.Otlp
@using Aspire.Dashboard.Otlp.Model
@using Aspire.Dashboard.Resources
@using Aspire.Dashboard.Utils
@using Aspire.Dashboard.Extensions
@using System.Globalization
@using Aspire.Dashboard.Components.Controls.Grid
@using Aspire.ResourceService.Proto.V1
@using Dialogs = Aspire.Dashboard.Resources.Dialogs
@implements IDialogContentComponent<InteractionsInputsDialogViewModel>

@inject IStringLocalizer<Dialogs> Loc

<FluentDialogHeader ShowDismiss="true">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should put the icon next to the header since it looks awkward next to the body. Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using the built-in message box from FluentUI: https://www.fluentui-blazor.net/MessageBox. Moving the icon would require creating a custom dialog.

We could do it in the future, but for now I think the default is good enough.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooooh I was looking at DialogBox in docs. Fine by me!

<FluentStack VerticalAlignment="VerticalAlignment.Center">
<FluentLabel Typo="Typography.PaneHeader">
@Dialog.Instance.Parameters.Title
</FluentLabel>
</FluentStack>
</FluentDialogHeader>

<FluentDialogBody Class="interaction-input-dialog">
@if (!string.IsNullOrEmpty(Content.Interaction.Message))
{
<p>@((MarkupString)Content.Interaction.Message)</p>
}

<EditForm EditContext="@_editContext">
<FluentStack Orientation="Orientation.Vertical" VerticalGap="12">
@foreach (var ss in Content.Inputs)
{
var localItem = ss;
<div class="interaction-input">
@switch (ss.InputType)
{
case InputType.Text:
<FluentTextField @bind-Value="localItem.Value" Label="@localItem.Label" Placeholder="@localItem.Placeholder" Required="localItem.Required" />
<ValidationMessage For="@(() => localItem.Value)" />
break;
case InputType.Password:
<FluentTextField @bind-Value="localItem.Value" Label="@localItem.Label" Placeholder="@localItem.Placeholder" Required="localItem.Required" TextFieldType="TextFieldType.Password" />
<ValidationMessage For="@(() => localItem.Value)" />
break;
case InputType.Select:
<FluentSelect TOption="SelectViewModel<string>"
@bind-Value="localItem.Value"
Label="@localItem.Label"
Placeholder="@localItem.Placeholder"
Required="localItem.Required"
Items="localItem.Options.Select(o => new SelectViewModel<string> { Id = o.Key, Name = o.Value })"
OptionValue="@(vm => vm.Id)"
OptionText="@(vm => vm.Name)"
Height="250px"
Position="SelectPosition.Below" />
<ValidationMessage For="@(() => localItem.Value)" />
break;
case InputType.Checkbox:
<FluentCheckbox @bind-Value="localItem.IsChecked" Label="@localItem.Label" Placeholder="@localItem.Placeholder" />
break;
case InputType.Number:
<FluentNumberField @bind-Value="localItem.NumberValue" Label="@localItem.Label" Placeholder="@localItem.Placeholder" Required="localItem.Required" />
<ValidationMessage For="@(() => localItem.NumberValue)" />
break;
default:
@* Ignore unexpected InputTypes *@
break;
}
</div>
}
</FluentStack>
</EditForm>
</FluentDialogBody>

<FluentDialogFooter>
<FluentButton Appearance="Appearance.Accent" OnClick="@OkAsync">
@Dialog.Instance.Parameters.PrimaryAction
</FluentButton>
@if (!string.IsNullOrEmpty(Dialog.Instance.Parameters.SecondaryAction))
{
<FluentButton Appearance="Appearance.Neutral" OnClick="@CancelAsync">
@Dialog.Instance.Parameters.SecondaryAction
</FluentButton>
}
</FluentDialogFooter>
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 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.Model;
using Aspire.ResourceService.Proto.V1;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.FluentUI.AspNetCore.Components;

namespace Aspire.Dashboard.Components.Dialogs;

public partial class InteractionsInputDialog
{
[Parameter]
public InteractionsInputsDialogViewModel Content { get; set; } = default!;

[CascadingParameter]
public FluentDialog Dialog { get; set; } = default!;

private EditContext _editContext = default!;
private ValidationMessageStore _validationMessages = default!;

protected override void OnInitialized()
{
_editContext = new EditContext(Content);
_validationMessages = new ValidationMessageStore(_editContext);

_editContext.OnValidationRequested += (s, e) => ValidateModel();
_editContext.OnFieldChanged += (s, e) => ValidateField(e.FieldIdentifier);
}

private void ValidateModel()
{
_validationMessages.Clear();

foreach (var inputModel in Content.Inputs)
{
var field = new FieldIdentifier(inputModel, nameof(inputModel.Value));
if (IsMissingRequiredValue(inputModel))
{
_validationMessages.Add(field, $"{inputModel.Label} is required.");
}
}

_editContext.NotifyValidationStateChanged();
}

private void ValidateField(FieldIdentifier field)
{
_validationMessages.Clear(field);

if (field.Model is InteractionInput inputModel)
{
if (IsMissingRequiredValue(inputModel))
{
_validationMessages.Add(field, $"{inputModel.Label} is required.");
}
}

_editContext.NotifyValidationStateChanged();
}

private static bool IsMissingRequiredValue(InteractionInput inputModel)
{
return inputModel.Required &&
inputModel.InputType != InputType.Checkbox &&
string.IsNullOrWhiteSpace(inputModel.Value);
}

private async Task OkAsync()
{
if (_editContext.Validate())
{
await Dialog.CloseAsync(Content);
}
}

private async Task CancelAsync()
{
await Dialog.CancelAsync();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.interaction-input-dialog .interaction-input ::deep {
width: 100%;
}

.interaction-input-dialog .interaction-input ::deep fluent-text-field, .interaction-input-dialog .interaction-input ::deep fluent-select {
width: 75%;
}
Loading
Loading