Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
13 changes: 7 additions & 6 deletions playground/Stress/Stress.AppHost/InteractionCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -796,8 +796,8 @@ public static IResourceBuilder<T> AddInteractionCommands<T>(this IResourceBuilde
var interactionService = commandContext.Services.GetRequiredService<IInteractionService>();

var result = await interactionService.PromptProgressAsync(
"Please wait while resources are being downloaded...",
"Downloading resources",
"Please wait while resources are being downloaded...",
new ProgressInteractionOptions
{
PrimaryButtonText = "Cancel",
Expand Down Expand Up @@ -825,8 +825,8 @@ public static IResourceBuilder<T> AddInteractionCommands<T>(this IResourceBuilde
var interactionService = commandContext.Services.GetRequiredService<IInteractionService>();

var result = await interactionService.PromptProgressAsync(
null,
"Please wait while resources are being downloaded...",
title: null,
options: new ProgressInteractionOptions
{
Work = async ctx =>
Expand Down Expand Up @@ -855,8 +855,8 @@ public static IResourceBuilder<T> AddInteractionCommands<T>(this IResourceBuilde
using var cts = CancellationTokenSource.CreateLinkedTokenSource(commandContext.CancellationToken);

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

// Simulate background work, then close the dialog.
Expand All @@ -877,8 +877,8 @@ public static IResourceBuilder<T> AddInteractionCommands<T>(this IResourceBuilde
var interactionService = commandContext.Services.GetRequiredService<IInteractionService>();

var result = await interactionService.PromptProgressAsync(
"Please wait while data is being loaded...",
"Loading",
"Please wait while data is being loaded...",
new ProgressInteractionOptions
{
Work = async ctx =>
Expand All @@ -900,6 +900,7 @@ public static IResourceBuilder<T> AddInteractionCommands<T>(this IResourceBuilde
var interactionService = commandContext.Services.GetRequiredService<IInteractionService>();

var result = await interactionService.PromptProgressAsync(
null,
"Please wait...",
options: new ProgressInteractionOptions
{
Expand All @@ -923,8 +924,8 @@ public static IResourceBuilder<T> AddInteractionCommands<T>(this IResourceBuilde
var interactionService = commandContext.Services.GetRequiredService<IInteractionService>();

var result = await interactionService.PromptProgressAsync(
"Provisioning resources for **MyApp**.\n\nThis may take several minutes.",
"Deploying to Azure",
"Provisioning resources for **MyApp**.\n\nThis may take several minutes.",
new ProgressInteractionOptions
{
PrimaryButtonText = "Abort deployment",
Expand All @@ -948,8 +949,8 @@ public static IResourceBuilder<T> AddInteractionCommands<T>(this IResourceBuilde
var interactionService = commandContext.Services.GetRequiredService<IInteractionService>();

var result = await interactionService.PromptProgressAsync(
"Building and pushing container images to registry. This will take approximately 30 seconds.",
"Building container images",
"Building and pushing container images to registry. This will take approximately 30 seconds.",
new ProgressInteractionOptions
{
PrimaryButtonText = "Cancel build"
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 @@ -454,7 +454,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.Title, progressOptions.Message, options: options, cancellationToken: cancellationToken).ConfigureAwait(false);

if (progressResult.Canceled)
{
Expand Down
8 changes: 6 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(options?.Title, 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 Down
4 changes: 2 additions & 2 deletions src/Aspire.Hosting/IInteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,16 @@ public interface IInteractionService
/// through the <see cref="ProgressContext.CancellationToken"/> provided to the work callback.
/// </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="message">The message to display in 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? title, string message, ProgressInteractionOptions? options = null, CancellationToken cancellationToken = default);
}

internal record QueueLoadOptions(
Expand Down
2 changes: 1 addition & 1 deletion 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? title, string message, ProgressInteractionOptions? options = null, CancellationToken cancellationToken = default)
{
EnsureServiceAvailable();

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
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,8 @@ impl InteractionInputsDialogOptions {
/// InteractionProgressOptions
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InteractionProgressOptions {
#[serde(rename = "Title", skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(rename = "PrimaryButtonText", skip_serializing_if = "Option::is_none")]
pub primary_button_text: Option<String>,
#[serde(rename = "EnableMessageMarkdown", skip_serializing_if = "Option::is_none")]
Expand All @@ -1199,6 +1201,9 @@ pub struct InteractionProgressOptions {
impl InteractionProgressOptions {
pub fn to_map(&self) -> HashMap<String, Value> {
let mut map = HashMap::new();
if let Some(ref v) = self.title {
map.insert("Title".to_string(), serde_json::to_value(v).unwrap_or(Value::Null));
}
if let Some(ref v) = self.primary_button_text {
map.insert("PrimaryButtonText".to_string(), serde_json::to_value(v).unwrap_or(Value::Null));
}
Expand Down Expand Up @@ -12181,13 +12186,10 @@ impl IInteractionService {
}

/// Displays a progress dialog with an indeterminate progress indicator.
pub fn prompt_progress(&self, message: &str, title: Option<&str>, options: Option<InteractionProgressOptions>, cancellation_token: Option<&CancellationToken>) -> Result<BoolInteractionResult, Box<dyn std::error::Error>> {
pub fn prompt_progress(&self, message: &str, options: Option<InteractionProgressOptions>, cancellation_token: Option<&CancellationToken>) -> Result<BoolInteractionResult, Box<dyn std::error::Error>> {
let mut args: HashMap<String, Value> = HashMap::new();
args.insert("interactionService".to_string(), self.handle.to_json());
args.insert("message".to_string(), serde_json::to_value(&message).unwrap_or(Value::Null));
if let Some(ref v) = title {
args.insert("title".to_string(), serde_json::to_value(v).unwrap_or(Value::Null));
}
if let Some(ref v) = options {
args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null));
}
Expand Down
Loading
Loading