Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
11 changes: 5 additions & 6 deletions playground/Stress/Stress.AppHost/InteractionCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -797,9 +797,9 @@ public static IResourceBuilder<T> AddInteractionCommands<T>(this IResourceBuilde

var result = await interactionService.PromptProgressAsync(
"Please wait while resources are being downloaded...",
"Downloading resources",
new ProgressInteractionOptions
{
Title = "Downloading resources",
PrimaryButtonText = "Cancel",
Work = async ctx =>
{
Expand All @@ -826,7 +826,6 @@ public static IResourceBuilder<T> AddInteractionCommands<T>(this IResourceBuilde

var result = await interactionService.PromptProgressAsync(
"Please wait while resources are being downloaded...",
title: null,
options: new ProgressInteractionOptions
{
Work = async ctx =>
Expand Down Expand Up @@ -856,7 +855,7 @@ public static IResourceBuilder<T> AddInteractionCommands<T>(this IResourceBuilde

var progressTask = interactionService.PromptProgressAsync(
"This dialog has no cancel button. It will close automatically.",
"Processing",
new ProgressInteractionOptions { Title = "Processing" },
cancellationToken: cts.Token);

// Simulate background work, then close the dialog.
Expand All @@ -878,9 +877,9 @@ public static IResourceBuilder<T> AddInteractionCommands<T>(this IResourceBuilde

var result = await interactionService.PromptProgressAsync(
"Please wait while data is being loaded...",
"Loading",
new ProgressInteractionOptions
{
Title = "Loading",
Work = async ctx =>
{
await Task.Delay(10000, ctx.CancellationToken);
Expand Down Expand Up @@ -924,9 +923,9 @@ public static IResourceBuilder<T> AddInteractionCommands<T>(this IResourceBuilde

var result = await interactionService.PromptProgressAsync(
"Provisioning resources for **MyApp**.\n\nThis may take several minutes.",
"Deploying to Azure",
new ProgressInteractionOptions
{
Title = "Deploying to Azure",
PrimaryButtonText = "Abort deployment",
EnableMessageMarkdown = true,
Work = async ctx =>
Expand All @@ -949,9 +948,9 @@ public static IResourceBuilder<T> AddInteractionCommands<T>(this IResourceBuilde

var result = await interactionService.PromptProgressAsync(
"Building and pushing container images to registry. This will take approximately 30 seconds.",
"Building container images",
new ProgressInteractionOptions
{
Title = "Building container images",
PrimaryButtonText = "Cancel build"
},
commandContext.CancellationToken);
Expand Down
10 changes: 4 additions & 6 deletions playground/TypeScriptAppHost/apphost.mts
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,10 @@ await cache.withCommand(
"Please wait while data is being processed...",
{
title: "Processing",
options: {
primaryButtonText: "Cancel",
work: async () => {
// Simulate a long-running operation.
await new Promise<void>(resolve => setTimeout(resolve, 10000));
}
primaryButtonText: "Cancel",
work: async () => {
// Simulate a long-running operation.
await new Promise<void>(resolve => setTimeout(resolve, 10000));
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ private async Task<ExecuteCommandResult> ExecuteCommandWithOptionalProgressAsync

var options = new ProgressInteractionOptions
{
Title = progressOptions.Title,
Comment thread
mitchdenny marked this conversation as resolved.
Work = async progress =>
{
// Use a linked token so that clicking Cancel in the progress dialog
Expand All @@ -454,7 +455,7 @@ private async Task<ExecuteCommandResult> ExecuteCommandWithOptionalProgressAsync
options.PrimaryButtonText = InteractionStrings.CommandProgressCancelButtonText;
}

var progressResult = await interactionService.PromptProgressAsync(progressOptions.Message, title: progressOptions.Title, options: options, cancellationToken: cancellationToken).ConfigureAwait(false);
var progressResult = await interactionService.PromptProgressAsync(progressOptions.Message, options: options, cancellationToken: cancellationToken).ConfigureAwait(false);

if (progressResult.Canceled)
{
Expand Down
9 changes: 7 additions & 2 deletions src/Aspire.Hosting/Ats/InteractionExports.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,12 @@ public static async Task<BoolInteractionResult> PromptNotification(
public static async Task<BoolInteractionResult> PromptProgress(
this IInteractionService interactionService,
string message,
string? title = null,
InteractionProgressOptions? options = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(interactionService);

var result = await interactionService.PromptProgressAsync(message, title, options?.ToOptions(), cancellationToken).ConfigureAwait(false);
var result = await interactionService.PromptProgressAsync(message, options?.ToOptions(), cancellationToken).ConfigureAwait(false);
return BoolInteractionResult.From(result);
}

Expand Down Expand Up @@ -747,6 +746,11 @@ internal InputsDialogInteractionOptions ToOptions()
[AspireDto]
internal sealed class InteractionProgressOptions
{
/// <summary>
/// Gets or sets the optional title of the progress dialog.
/// </summary>
public string? Title { get; init; }

/// <summary>
/// Gets or sets the primary button text (e.g. "Cancel").
/// </summary>
Expand All @@ -768,6 +772,7 @@ internal ProgressInteractionOptions ToOptions()
{
return new ProgressInteractionOptions
{
Title = Title,
Comment thread
mitchdenny marked this conversation as resolved.
PrimaryButtonText = PrimaryButtonText,
EnableMessageMarkdown = EnableMessageMarkdown,
Work = Work,
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Aspire.Hosting.IInteractionService.PromptProgressAsync(System.String,System.String,Aspire.Hosting.ProgressInteractionOptions,System.Threading.CancellationToken)</Target>
<Target>M:Aspire.Hosting.IInteractionService.PromptProgressAsync(System.String,Aspire.Hosting.ProgressInteractionOptions,System.Threading.CancellationToken)</Target>
<Left>lib/net8.0/Aspire.Hosting.dll</Left>
<Right>lib/net8.0/Aspire.Hosting.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
Expand Down
8 changes: 6 additions & 2 deletions src/Aspire.Hosting/IInteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,14 @@ public interface IInteractionService
/// </para>
/// </remarks>
/// <param name="message">The message to display in the progress dialog.</param>
/// <param name="title">The optional title of the progress dialog.</param>
/// <param name="options">Optional configuration for the progress interaction.</param>
/// <param name="cancellationToken">A token to cancel the operation and close the dialog.</param>
/// <returns>
/// An <see cref="InteractionResult{T}"/> containing <c>true</c> if the operation completed successfully,
/// or a canceled result if the user clicked the cancel button.
/// </returns>
[Experimental("ASPIREINTERACTION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
Task<InteractionResult<bool>> PromptProgressAsync(string message, string? title = null, ProgressInteractionOptions? options = null, CancellationToken cancellationToken = default);
Task<InteractionResult<bool>> PromptProgressAsync(string message, ProgressInteractionOptions? options = null, CancellationToken cancellationToken = default);
Comment thread
JamesNK marked this conversation as resolved.
Comment thread
mitchdenny marked this conversation as resolved.
}

internal record QueueLoadOptions(
Expand Down Expand Up @@ -802,6 +801,11 @@ public class ProgressInteractionOptions : InteractionOptions
{
internal static ProgressInteractionOptions CreateDefault() => new();

/// <summary>
/// Gets or sets the optional title of the progress dialog.
/// </summary>
public string? Title { get; set; }

/// <summary>
/// Gets or sets an optional asynchronous work callback to execute while the progress dialog is displayed.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Hosting/InteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ public async Task<InteractionResult<bool>> PromptNotificationAsync(string title,
}
}

public async Task<InteractionResult<bool>> PromptProgressAsync(string message, string? title = null, ProgressInteractionOptions? options = null, CancellationToken cancellationToken = default)
public async Task<InteractionResult<bool>> PromptProgressAsync(string message, ProgressInteractionOptions? options = null, CancellationToken cancellationToken = default)
{
EnsureServiceAvailable();

Expand All @@ -295,7 +295,7 @@ public async Task<InteractionResult<bool>> PromptProgressAsync(string message, s
{
options ??= ProgressInteractionOptions.CreateDefault();

var newState = new Interaction(title ?? string.Empty, message, options, new Interaction.ProgressInteractionInfo(), interactionCts.Token);
var newState = new Interaction(options.Title ?? string.Empty, message, options, new Interaction.ProgressInteractionInfo(), interactionCts.Token);
AddInteractionUpdate(newState);

using var ctRegistration = cancellationToken.Register(OnInteractionCancellation, state: newState);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ BREAK capability-removed Aspire.Hosting Aspire.Hosting.ApplicationModel/UpdateCo
BREAK dto-property-added-required Aspire.Hosting Aspire.Hosting/Aspire.Hosting.Ats.HttpsCertificateExecutionConfigurationExportData.IsCertificateWithKeyPathReferenced -- https://github.com/microsoft/aspire/pull/18196 -- Additive flag on the host-produced getHttpsCertificateData() result DTO; output-only data the host always populates, so consuming polyglot SDKs are unaffected (the direction-agnostic checker flags every newly required DTO property).
BREAK dto-property-added-required Aspire.Hosting Aspire.Hosting/Aspire.Hosting.ApplicationModel.HttpsCertificateExecutionConfigurationContext.CertificateWithKeyPath -- https://github.com/microsoft/aspire/pull/18196 -- Additive member on the [Experimental("ASPIRECERTIFICATES001")] HTTPS certificate config surface; experimental opt-in APIs carry no compatibility guarantee (C# compat ignores it for the same reason). The ATS checker has no experimental awareness, so it flags the new required property.
BREAK dto-property-added-required Aspire.Hosting Aspire.Hosting/Aspire.Hosting.ApplicationModel.CommandOptions.Progress -- https://github.com/microsoft/aspire/pull/18493 -- Additive nullable/optional property on the stable CommandOptions [AspireDto]. The property is optional at runtime (null means no progress dialog) and existing polyglot apphosts are unaffected; the direction-agnostic ATS checker flags it because it cannot distinguish optional from required DTO properties.
BREAK capability-parameter-removed Aspire.Hosting Aspire.Hosting/promptProgress(title) -- https://github.com/microsoft/aspire/pull/19382 -- promptProgress is a new, unshipped experimental capability; moving its optional title into InteractionProgressOptions does not break released polyglot callers.
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,7 @@ func (d *InteractionInputsDialogOptions) ToMap() map[string]any {

// InteractionProgressOptions represents InteractionProgressOptions.
type InteractionProgressOptions struct {
Title *string `json:"Title,omitempty"`
PrimaryButtonText *string `json:"PrimaryButtonText,omitempty"`
EnableMessageMarkdown *bool `json:"EnableMessageMarkdown,omitempty"`
Work func(arg ProgressContext) `json:"Work,omitempty"`
Expand All @@ -608,6 +609,7 @@ type InteractionProgressOptions struct {
// ToMap converts the DTO to a map for JSON serialization.
func (d *InteractionProgressOptions) ToMap() map[string]any {
m := map[string]any{}
if d.Title != nil { m["Title"] = serializeValue(d.Title) }
if d.PrimaryButtonText != nil { m["PrimaryButtonText"] = serializeValue(d.PrimaryButtonText) }
if d.EnableMessageMarkdown != nil { m["EnableMessageMarkdown"] = serializeValue(d.EnableMessageMarkdown) }
if d.Work != nil {
Expand Down Expand Up @@ -29319,15 +29321,13 @@ func (o *PromptNotificationOptions) ToMap() map[string]any {

// PromptProgressOptions carries optional parameters for PromptProgress.
type PromptProgressOptions struct {
Title *string `json:"title,omitempty"`
Options *InteractionProgressOptions `json:"options,omitempty"`
CancellationToken *CancellationToken `json:"-"`
}

func (o *PromptProgressOptions) ToMap() map[string]any {
m := map[string]any{}
if o == nil { return m }
if o.Title != nil { m["title"] = serializeValue(o.Title) }
if o.Options != nil { m["options"] = serializeValue(o.Options) }
return m
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15355,24 +15355,20 @@ private BoolInteractionResult promptNotificationImpl(String title, String messag

/** Displays a progress dialog with an indeterminate progress indicator. */
public BoolInteractionResult promptProgress(String message, PromptProgressOptions optionsBag) {
var title = optionsBag == null ? null : optionsBag.getTitle();
var options = optionsBag == null ? null : optionsBag.getOptions();
var cancellationToken = optionsBag == null ? null : optionsBag.getCancellationToken();
return promptProgressImpl(message, title, options, cancellationToken);
return promptProgressImpl(message, options, cancellationToken);
}

public BoolInteractionResult promptProgress(String message) {
return promptProgress(message, null);
}

/** Displays a progress dialog with an indeterminate progress indicator. */
private BoolInteractionResult promptProgressImpl(String message, String title, InteractionProgressOptions options, CancellationToken cancellationToken) {
private BoolInteractionResult promptProgressImpl(String message, InteractionProgressOptions options, CancellationToken cancellationToken) {
Map<String, Object> reqArgs = new HashMap<>();
reqArgs.put("interactionService", AspireClient.serializeValue(getHandle()));
reqArgs.put("message", AspireClient.serializeValue(message));
if (title != null) {
reqArgs.put("title", AspireClient.serializeValue(title));
}
if (options != null) {
reqArgs.put("options", AspireClient.serializeValue(options));
}
Expand Down Expand Up @@ -16984,10 +16980,13 @@ public Map<String, Object> toMap() {

/** InteractionProgressOptions DTO. */
public class InteractionProgressOptions implements JsonSerializable {
private String title;
private String primaryButtonText;
private Boolean enableMessageMarkdown;
private AspireAction1<ProgressContext> work;

public String getTitle() { return title; }
public void setTitle(String value) { this.title = value; }
public String getPrimaryButtonText() { return primaryButtonText; }
public void setPrimaryButtonText(String value) { this.primaryButtonText = value; }
public Boolean getEnableMessageMarkdown() { return enableMessageMarkdown; }
Expand All @@ -16998,6 +16997,8 @@ public class InteractionProgressOptions implements JsonSerializable {
@SuppressWarnings("unchecked")
public static InteractionProgressOptions fromMap(Map<String, Object> map) {
var value = new InteractionProgressOptions();
var titleValue = map.get("Title");
value.setTitle(titleValue == null ? null : (String) titleValue);
var primaryButtonTextValue = map.get("PrimaryButtonText");
value.setPrimaryButtonText(primaryButtonTextValue == null ? null : (String) primaryButtonTextValue);
var enableMessageMarkdownValue = map.get("EnableMessageMarkdown");
Expand All @@ -17007,6 +17008,7 @@ public static InteractionProgressOptions fromMap(Map<String, Object> map) {

public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();
map.put("Title", AspireClient.serializeValue(title));
map.put("PrimaryButtonText", AspireClient.serializeValue(primaryButtonText));
map.put("EnableMessageMarkdown", AspireClient.serializeValue(enableMessageMarkdown));
map.put("Work", work == null ? null : (java.util.function.Function<Object, Object>) (transportArg -> {
Expand Down Expand Up @@ -20622,16 +20624,9 @@ public PromptNotificationOptions cancellationToken(CancellationToken value) {

/** Options for PromptProgress. */
public final class PromptProgressOptions {
private String title;
private InteractionProgressOptions options;
private CancellationToken cancellationToken;

public String getTitle() { return title; }
public PromptProgressOptions title(String value) {
this.title = value;
return this;
}

public InteractionProgressOptions getOptions() { return options; }
public PromptProgressOptions options(InteractionProgressOptions value) {
this.options = value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1949,6 +1949,7 @@ class InteractionNotificationOptions(typing.TypedDict, total=False):
LinkUrl: str | None

class InteractionProgressOptions(typing.TypedDict, total=False):
Title: str | None
PrimaryButtonText: str | None
EnableMessageMarkdown: bool | None
Work: typing.Callable[[ProgressContext], None]
Expand Down Expand Up @@ -3015,12 +3016,10 @@ def prompt_notification(self, title: str, message: str, *, options: InteractionN
)
return typing.cast(BoolInteractionResult, result)

def prompt_progress(self, message: str, *, title: str | None = None, options: InteractionProgressOptions | None = None, timeout: int | None = None) -> BoolInteractionResult:
def prompt_progress(self, message: str, *, options: InteractionProgressOptions | None = None, timeout: int | None = None) -> BoolInteractionResult:
"""Displays a progress dialog with an indeterminate progress indicator."""
rpc_args: dict[str, typing.Any] = {'interactionService': self._handle}
rpc_args['message'] = message
if title is not None:
rpc_args['title'] = title
if options is not None:
rpc_args['options'] = options
if timeout is not None:
Expand Down
Loading
Loading