diff --git a/playground/Stress/Stress.AppHost/AppHost.cs b/playground/Stress/Stress.AppHost/AppHost.cs index 7c621547f37..40da074fa3b 100644 --- a/playground/Stress/Stress.AppHost/AppHost.cs +++ b/playground/Stress/Stress.AppHost/AppHost.cs @@ -140,7 +140,12 @@ issuedAt = DateTime.UtcNow }; var json = JsonSerializer.Serialize(token, new JsonSerializerOptions { WriteIndented = true }); - return Task.FromResult(CommandResults.Success(json, CommandResultFormat.Json)); + var resultData = new CommandResultData + { + Value = json, + Format = CommandResultFormat.Json + }; + return Task.FromResult(CommandResults.Success("Generated token.", resultData)); }, commandOptions: new() { IconName = "Key", Description = "Generate a temporary access token" }) .WithCommand( @@ -149,7 +154,18 @@ executeCommand: (c) => { var connectionString = $"Server=localhost,1433;Database=StressDb;User Id=sa;Password={Guid.NewGuid():N};TrustServerCertificate=true"; - return Task.FromResult(CommandResults.Success(connectionString, CommandResultFormat.Text)); + var message = """ + Retrieved connection string. The database connection was established successfully + after verifying TLS certificates and negotiating encryption parameters. + + The server responded with protocol version 7.4 and confirmed support for multiple + active result sets. Connection pooling is enabled with a maximum pool size of 100 + connections and a minimum of 10 idle connections maintained. + + The login handshake completed in 42ms with SSPI authentication. All pre-login + checks passed including network library validation and instance name resolution. + """; + return Task.FromResult(CommandResults.Success(message, new CommandResultData { Value = connectionString, DisplayImmediately = true })); }, commandOptions: new() { IconName = "LinkMultiple", Description = "Get the connection string for this resource" }) .WithCommand( @@ -169,7 +185,33 @@ { return Task.FromResult(CommandResults.Failure("Health check failed", "Connection refused: ECONNREFUSED 127.0.0.1:5432\nRetries exhausted after 3 attempts", CommandResultFormat.Text)); }, - commandOptions: new() { IconName = "HeartBroken", Description = "Check resource health (always fails with details)" }); + commandOptions: new() { IconName = "HeartBroken", Description = "Check resource health (always fails with details)" }) + .WithCommand( + name: "migrate-database", + displayName: "Migrate Database", + executeCommand: (c) => + { + var markdown = """ + # ⚙️ Database Migration Summary + + | Table | Result | + |------------|----------------------------| + | Customers | ✅ 1,200 rows | + | Products | ✅ 850 rows | + | Orders | ✅ 3,400 rows | + | OrderItems | ✅ 8,750 rows | + | Categories | ✅ 45 rows | + | Reviews | ❌ FK constraint violation | + | Inventory | ✅ 850 rows | + | Shipping | ✅ 3,400 rows | + | Payments | ❌ Timeout after 30s | + | Coupons | ✅ 120 rows | + + **Summary:** 8 of 10 tables migrated successfully. 2 tables failed. + """; + return Task.FromResult(CommandResults.Success("Database migrated.", new CommandResultData { Value = markdown, Format = CommandResultFormat.Markdown })); + }, + commandOptions: new() { IconName = "CloudDatabase", Description = "Migrate the database with sample store data" }); #if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging diff --git a/src/Aspire.Cli/Commands/ResourceCommandHelper.cs b/src/Aspire.Cli/Commands/ResourceCommandHelper.cs index 67e6865dd32..66b019ec436 100644 --- a/src/Aspire.Cli/Commands/ResourceCommandHelper.cs +++ b/src/Aspire.Cli/Commands/ResourceCommandHelper.cs @@ -77,13 +77,15 @@ public static async Task ExecuteGenericCommandAsync( } else { - var errorMessage = GetFriendlyErrorMessage(response.ErrorMessage); +#pragma warning disable CS0618 // Type or member is obsolete + var errorMessage = GetFriendlyErrorMessage(response.Message ?? response.ErrorMessage); +#pragma warning restore CS0618 // Type or member is obsolete interactionService.DisplayError($"Failed to execute command '{commandName}' on resource '{resourceName}': {errorMessage}"); } - if (response.Result is not null) + if (response.Value is not null) { - interactionService.DisplayRawText(response.Result, ConsoleOutput.Standard); + DisplayCommandResult(interactionService, response.Value); } return response.Success ? ExitCodeConstants.Success : ExitCodeConstants.FailedToExecuteResourceCommand; @@ -108,18 +110,32 @@ private static int HandleResponse( } else { - var errorMessage = GetFriendlyErrorMessage(response.ErrorMessage); +#pragma warning disable CS0618 // Type or member is obsolete + var errorMessage = GetFriendlyErrorMessage(response.Message ?? response.ErrorMessage); +#pragma warning restore CS0618 // Type or member is obsolete interactionService.DisplayError($"Failed to {baseVerb} resource '{resourceName}': {errorMessage}"); } - if (response.Result is not null) + if (response.Value is not null) { - interactionService.DisplayRawText(response.Result, ConsoleOutput.Standard); + DisplayCommandResult(interactionService, response.Value); } return response.Success ? ExitCodeConstants.Success : ExitCodeConstants.FailedToExecuteResourceCommand; } + private static void DisplayCommandResult(IInteractionService interactionService, ExecuteResourceCommandResult result) + { + if (result.Format is CommandResultFormat.Markdown) + { + interactionService.DisplayMarkdown(result.Value, ConsoleOutput.Standard); + } + else + { + interactionService.DisplayRawText(result.Value, ConsoleOutput.Standard); + } + } + private static string GetFriendlyErrorMessage(string? errorMessage) { return string.IsNullOrEmpty(errorMessage) ? "Unknown error occurred." : errorMessage; diff --git a/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs b/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs index 8b39ccbc961..52eb221ae8e 100644 --- a/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs +++ b/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs @@ -352,10 +352,12 @@ public void DisplayRawText(string text, ConsoleOutput? consoleOverride = null) target.Profile.Out.Writer.WriteLine(text); } - public void DisplayMarkdown(string markdown) + public void DisplayMarkdown(string markdown, ConsoleOutput? consoleOverride = null) { + var effectiveConsole = consoleOverride ?? Console; + var target = effectiveConsole == ConsoleOutput.Error ? _errorConsole : _outConsole; var spectreMarkup = MarkdownToSpectreConverter.ConvertToSpectre(markdown); - MessageConsole.MarkupLine(spectreMarkup); + target.MarkupLine(spectreMarkup); } public void DisplayMarkupLine(string markup) diff --git a/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs b/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs index c6060ec1496..de655d5e75d 100644 --- a/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs +++ b/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs @@ -391,13 +391,13 @@ public void DisplayRawText(string text, ConsoleOutput? consoleOverride = null) _consoleInteractionService.DisplayRawText(text, consoleOverride); } - public void DisplayMarkdown(string markdown) + public void DisplayMarkdown(string markdown, ConsoleOutput? consoleOverride = null) { // Send raw markdown to extension (it can handle markdown natively) // Convert to Spectre markup for console display var result = _extensionTaskChannel.Writer.TryWrite(() => Backchannel.LogMessageAsync(LogLevel.Information, markdown, _cancellationToken)); Debug.Assert(result); - _consoleInteractionService.DisplayMarkdown(markdown); + _consoleInteractionService.DisplayMarkdown(markdown, consoleOverride); } public void DisplayMarkupLine(string markup) diff --git a/src/Aspire.Cli/Interaction/IInteractionService.cs b/src/Aspire.Cli/Interaction/IInteractionService.cs index 0b2711e4adb..a851a2a5bea 100644 --- a/src/Aspire.Cli/Interaction/IInteractionService.cs +++ b/src/Aspire.Cli/Interaction/IInteractionService.cs @@ -22,7 +22,7 @@ internal interface IInteractionService void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false); void DisplayPlainText(string text); void DisplayRawText(string text, ConsoleOutput? consoleOverride = null); - void DisplayMarkdown(string markdown); + void DisplayMarkdown(string markdown, ConsoleOutput? consoleOverride = null); void DisplayMarkupLine(string markup); void DisplaySuccess(string message, bool allowMarkup = false); void DisplaySubtleMessage(string message, bool allowMarkup = false); diff --git a/src/Aspire.Cli/Mcp/Tools/ExecuteResourceCommandTool.cs b/src/Aspire.Cli/Mcp/Tools/ExecuteResourceCommandTool.cs index 916c1d95dd1..50d255bd10f 100644 --- a/src/Aspire.Cli/Mcp/Tools/ExecuteResourceCommandTool.cs +++ b/src/Aspire.Cli/Mcp/Tools/ExecuteResourceCommandTool.cs @@ -79,9 +79,9 @@ public override async ValueTask CallToolAsync(CallToolContext co new() { Text = $"Command '{commandName}' executed successfully on resource '{resourceName}'." } }; - if (response.Result is not null) + if (response.Value is not null) { - content.Add(new TextContentBlock { Text = response.Result }); + content.Add(new TextContentBlock { Text = response.Value.Value }); } return new CallToolResult @@ -95,16 +95,18 @@ public override async ValueTask CallToolAsync(CallToolContext co } else { - var message = response.ErrorMessage is { Length: > 0 } ? response.ErrorMessage : "Unknown error. See logs for details."; +#pragma warning disable CS0618 // Type or member is obsolete + var message = (response.Message ?? response.ErrorMessage) is { Length: > 0 } errorMsg ? errorMsg : "Unknown error. See logs for details."; +#pragma warning restore CS0618 // Type or member is obsolete var content = new List { new() { Text = $"Command '{commandName}' failed for resource '{resourceName}': {message}" } }; - if (response.Result is not null) + if (response.Value is not null) { - content.Add(new TextContentBlock { Text = response.Result }); + content.Add(new TextContentBlock { Text = response.Value.Value }); } return new CallToolResult diff --git a/src/Aspire.Dashboard/Components/Dialogs/NotificationEntryComponent.razor b/src/Aspire.Dashboard/Components/Dialogs/NotificationEntryComponent.razor new file mode 100644 index 00000000000..a6eec31b250 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Dialogs/NotificationEntryComponent.razor @@ -0,0 +1,44 @@ +@using Aspire.Dashboard.Model +@using Aspire.Dashboard.Otlp.Model +@using Aspire.Dashboard.Resources +@using Microsoft.FluentUI.AspNetCore.Components.Extensions +@inject TimeProvider TimeProvider +@inject IStringLocalizer Loc + +
+ @* Header *@ +
+ +
+
+ @Entry.Title +
+
+ + + +
+ + @* Detailed content *@ +
+ @if (!string.IsNullOrEmpty(Entry.Body)) + { + @OtlpHelpers.TruncateString(Entry.Body, 500) + } + @if (Entry.PrimaryAction is { } primaryAction) + { + + @primaryAction.Text + + } +
+ + @* Timestamp *@ +
+ @((TimeProvider.GetUtcNow() - Entry.Timestamp).ToTimeAgo()) +
+
diff --git a/src/Aspire.Dashboard/Components/Dialogs/NotificationEntryComponent.razor.cs b/src/Aspire.Dashboard/Components/Dialogs/NotificationEntryComponent.razor.cs new file mode 100644 index 00000000000..3ee4f90d268 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Dialogs/NotificationEntryComponent.razor.cs @@ -0,0 +1,55 @@ +// 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 Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components; +using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons; + +namespace Aspire.Dashboard.Components.Dialogs; + +public partial class NotificationEntryComponent : ComponentBase +{ + [Parameter, EditorRequired] + public required NotificationEntry Entry { get; set; } + + [Parameter] + public EventCallback OnDismiss { get; set; } + + private string IntentClass => Entry.Intent switch + { + MessageIntent.Success => "intent-success", + MessageIntent.Error => "intent-error", + MessageIntent.Warning => "intent-warning", + _ => "intent-info" + }; + + private Icon Icon => Entry.Intent switch + { + MessageIntent.Success => new Icons.Filled.Size20.CheckmarkCircle(), + MessageIntent.Error => new Icons.Filled.Size20.DismissCircle(), + MessageIntent.Warning => new Icons.Filled.Size20.Warning(), + _ => new Icons.Filled.Size20.Info() + }; + + private Color IconColor => Entry.Intent switch + { + MessageIntent.Success => Color.Success, + MessageIntent.Error => Color.Error, + MessageIntent.Warning => Color.Warning, + _ => Color.Info + }; + + private async Task HandleDismiss() + { + await OnDismiss.InvokeAsync(); + } + + private async Task HandlePrimaryAction() + { + if (Entry.PrimaryAction is { } primaryAction) + { + await primaryAction.OnClick(); + } + } +} diff --git a/src/Aspire.Dashboard/Components/Dialogs/NotificationEntryComponent.razor.css b/src/Aspire.Dashboard/Components/Dialogs/NotificationEntryComponent.razor.css new file mode 100644 index 00000000000..57e1eec5b1c --- /dev/null +++ b/src/Aspire.Dashboard/Components/Dialogs/NotificationEntryComponent.razor.css @@ -0,0 +1,82 @@ +.notification-entry { + font-family: var(--body-font); + color: var(--neutral-foreground-rest); + display: grid; + grid-template-columns: 24px 1fr auto; + width: 100%; + min-height: 36px; + padding: 0 8px; + column-gap: 4px; + border-top: calc(var(--stroke-width) * 1px) solid var(--neutral-stroke-divider-rest); +} + +.notification-entry.intent-info { + fill: var(--info); +} + +.notification-entry.intent-warning { + fill: var(--warning); +} + +.notification-entry.intent-error { + fill: var(--error); +} + +.notification-entry.intent-success { + fill: var(--success); +} + +.notification-entry-icon { + grid-column: 1; + grid-row: 1; + display: flex; + justify-content: center; + align-self: center; +} + +.notification-entry-message { + grid-column: 2; + grid-row: 1; + padding: 10px 0; + align-self: center; + font-weight: 600; + font-size: 12px; + line-height: 16px; + white-space: unset; +} + +.notification-entry-close { + grid-column: 3; + grid-row: 1; + padding: 4px; + display: flex; + justify-content: center; + justify-self: center; + cursor: pointer; +} + +.notification-entry-content { + grid-column: 1 / 4; + grid-row: 2; + padding: 6px 6px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + font-size: 12px; + word-break: break-word; +} + +.notification-entry-time { + grid-column: 2 / 4; + grid-row: 3; + font-size: 12px; + text-align: right; + padding: 4px 4px 8px 0px; + color: var(--foreground-subtext-rest); +} + +::deep .notification-entry-action { + height: 24px; + font-size: 12px; +} diff --git a/src/Aspire.Dashboard/Components/Dialogs/NotificationsDialog.razor b/src/Aspire.Dashboard/Components/Dialogs/NotificationsDialog.razor new file mode 100644 index 00000000000..74fbbe0895b --- /dev/null +++ b/src/Aspire.Dashboard/Components/Dialogs/NotificationsDialog.razor @@ -0,0 +1,29 @@ +@using Aspire.Dashboard.Resources +@using Aspire.Dashboard.Utils +@inject IStringLocalizer Loc + + +
+ @if (_notifications.Count == 0) + { +
+ +

@Loc[nameof(Dialogs.NotificationCenterEmpty)]

+
+ } + else + { +
+ + @Loc[nameof(Dialogs.NotificationCenterDismissAll)] + +
+
+ @foreach (var notification in _notifications) + { + + } +
+ } +
+
diff --git a/src/Aspire.Dashboard/Components/Dialogs/NotificationsDialog.razor.cs b/src/Aspire.Dashboard/Components/Dialogs/NotificationsDialog.razor.cs new file mode 100644 index 00000000000..d766b7fcf02 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Dialogs/NotificationsDialog.razor.cs @@ -0,0 +1,50 @@ +// 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 Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components; + +namespace Aspire.Dashboard.Components.Dialogs; + +public partial class NotificationsDialog : IDialogContentComponent, IDisposable +{ + private IReadOnlyList _notifications = []; + + [Inject] + public required INotificationService NotificationService { get; init; } + + [CascadingParameter] + public FluentDialog Dialog { get; set; } = default!; + + protected override void OnInitialized() + { + _notifications = NotificationService.GetNotifications(); + NotificationService.OnChange += HandleNotificationsChanged; + NotificationService.ResetUnreadCount(); + } + + private void HandleNotificationsChanged() + { + _ = InvokeAsync(() => + { + _notifications = NotificationService.GetNotifications(); + StateHasChanged(); + }); + } + + private void DismissAll() + { + NotificationService.ClearAll(); + } + + private void Dismiss(string id) + { + NotificationService.RemoveNotification(id); + } + + public void Dispose() + { + NotificationService.OnChange -= HandleNotificationsChanged; + } +} diff --git a/src/Aspire.Dashboard/Components/Dialogs/NotificationsDialog.razor.css b/src/Aspire.Dashboard/Components/Dialogs/NotificationsDialog.razor.css new file mode 100644 index 00000000000..1cc4a07cc58 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Dialogs/NotificationsDialog.razor.css @@ -0,0 +1,31 @@ +.notifications-container { + display: flex; + flex-direction: column; + height: 100%; + gap: 8px; +} + +.notification-center-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 16px; + color: var(--neutral-foreground-hint); + gap: 8px; +} + +.notifications-dismiss { + display: flex; + justify-content: flex-end; +} + +.notifications-scroll { + overflow-y: auto; + flex: 1; + min-height: 0; +} + +.notifications-scroll ::deep > :last-child { + border-bottom: calc(var(--stroke-width) * 1px) solid var(--neutral-stroke-divider-rest); +} diff --git a/src/Aspire.Dashboard/Components/Dialogs/TextVisualizerDialog.razor b/src/Aspire.Dashboard/Components/Dialogs/TextVisualizerDialog.razor index a62c4fdde0f..458820cbc36 100644 --- a/src/Aspire.Dashboard/Components/Dialogs/TextVisualizerDialog.razor +++ b/src/Aspire.Dashboard/Components/Dialogs/TextVisualizerDialog.razor @@ -35,7 +35,16 @@
@if (IsTextContentDisplayed) { - + if (IsMarkdownFormat) + { +
+ +
+ } + else + { + + } } else if (ShowSecretsWarning is true) { diff --git a/src/Aspire.Dashboard/Components/Dialogs/TextVisualizerDialog.razor.cs b/src/Aspire.Dashboard/Components/Dialogs/TextVisualizerDialog.razor.cs index e90dd7743d4..b4a563cc018 100644 --- a/src/Aspire.Dashboard/Components/Dialogs/TextVisualizerDialog.razor.cs +++ b/src/Aspire.Dashboard/Components/Dialogs/TextVisualizerDialog.razor.cs @@ -3,6 +3,7 @@ using Aspire.Dashboard.Extensions; using Aspire.Dashboard.Model; +using Aspire.Dashboard.Model.Markdown; using Aspire.Dashboard.Utils; using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; @@ -18,10 +19,12 @@ public partial class TextVisualizerDialog : ComponentBase private List> _options = null!; private string? _selectedFormat; private bool _isLoading = true; + private MarkdownProcessor? _markdownProcessor; internal TextVisualizerViewModel TextVisualizerViewModel { get; set; } = default!; public HashSet EnabledOptions { get; } = []; internal bool? ShowSecretsWarning { get; private set; } + internal bool IsMarkdownFormat => TextVisualizerViewModel.FormatKind == DashboardUIHelpers.MarkdownFormat; /// /// Returns true if the dialog has a fixed format that cannot be changed by the user. @@ -56,9 +59,11 @@ protected override void OnParametersSet() { EnabledOptions.Clear(); EnabledOptions.Add(DashboardUIHelpers.PlaintextFormat); + EnabledOptions.Add(DashboardUIHelpers.MarkdownFormat); _options = [ new SelectViewModel { Id = DashboardUIHelpers.PlaintextFormat, Name = Loc[nameof(Resources.Dialogs.TextVisualizerDialogPlaintextFormat)] }, + new SelectViewModel { Id = DashboardUIHelpers.MarkdownFormat, Name = Loc[nameof(Resources.Dialogs.TextVisualizerDialogMarkdownFormat)] }, new SelectViewModel { Id = DashboardUIHelpers.JsonFormat, Name = Loc[nameof(Resources.Dialogs.TextVisualizerDialogJsonFormat)] }, new SelectViewModel { Id = DashboardUIHelpers.XmlFormat, Name = Loc[nameof(Resources.Dialogs.TextVisualizerDialogXmlFormat)] } ]; @@ -104,6 +109,11 @@ public void ChangeFormat(string? newFormat, string? text) TextVisualizerViewModel.UpdateFormat(newFormat ?? DashboardUIHelpers.PlaintextFormat); } + internal MarkdownProcessor GetMarkdownProcessor() + { + return _markdownProcessor ??= new MarkdownProcessor(ControlsStringsLoc, safeUrlSchemes: MarkdownHelpers.SafeUrlSchemes, extensions: []); + } + public static async Task OpenDialogAsync(OpenTextVisualizerDialogOptions options) { var width = options.DialogService.IsDesktop ? "75vw" : "100vw"; diff --git a/src/Aspire.Dashboard/Components/Dialogs/TextVisualizerDialog.razor.css b/src/Aspire.Dashboard/Components/Dialogs/TextVisualizerDialog.razor.css index d2928542350..ffdb9e26894 100644 --- a/src/Aspire.Dashboard/Components/Dialogs/TextVisualizerDialog.razor.css +++ b/src/Aspire.Dashboard/Components/Dialogs/TextVisualizerDialog.razor.css @@ -34,3 +34,9 @@ max-height: 60vh; margin-bottom: 16px; } + +.markdown-content { + max-height: 60vh; + overflow-y: auto; + margin-bottom: 16px; +} diff --git a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor index d8b881c611e..e54115cfdb9 100644 --- a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor +++ b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor @@ -41,6 +41,7 @@ } + @@ -79,6 +80,7 @@ CloseNavMenu="@CloseMobileNavMenu" LaunchHelpAsync="@LaunchHelpAsync" LaunchAIAssistantAsync="@LaunchAssistantAsync" + LaunchNotificationsAsync="@LaunchNotificationsAsync" LaunchSettingsAsync="@LaunchSettingsAsync" /> } diff --git a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs index fed6d140e8e..17abc409ac6 100644 --- a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs +++ b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs @@ -30,6 +30,7 @@ public partial class MainLayout : IGlobalKeydownListener, IAsyncDisposable private IDisposable? _aiDisplayChangedSubscription; private const string SettingsDialogId = "SettingsDialog"; private const string HelpDialogId = "HelpDialog"; + private const string NotificationsDialogId = "NotificationsDialog"; [Inject] public required ThemeManager ThemeManager { get; init; } @@ -285,6 +286,42 @@ public async Task LaunchSettingsAsync() } } + public async Task LaunchNotificationsAsync() + { + var parameters = new DialogParameters + { + Title = Loc[nameof(Resources.Layout.MainLayoutNotificationCenterTitle)], + PrimaryAction = Loc[nameof(Resources.Layout.MainLayoutSettingsDialogClose)].Value, + SecondaryAction = null, + TrapFocus = true, + Modal = true, + Alignment = HorizontalAlignment.Right, + Width = "350px", + Height = "auto", + Id = NotificationsDialogId, + OnDialogClosing = EventCallback.Factory.Create(this, HandleDialogClose) + }; + + if (_openPageDialog is not null) + { + if (Equals(_openPageDialog.Id, NotificationsDialogId) && !_openPageDialog.Result.IsCompleted) + { + return; + } + + await _openPageDialog.CloseAsync(); + } + + if (ViewportInformation.IsDesktop) + { + _openPageDialog = await DialogService.ShowPanelAsync(parameters).ConfigureAwait(true); + } + else + { + _openPageDialog = await DialogService.ShowDialogAsync(parameters).ConfigureAwait(true); + } + } + public async Task LaunchAssistantAsync() { if (AIContextProvider.AssistantChatViewModel != null && AIContextProvider.ShowAssistantSidebarDialog) diff --git a/src/Aspire.Dashboard/Components/Layout/MobileNavMenu.razor b/src/Aspire.Dashboard/Components/Layout/MobileNavMenu.razor index 56a6927d05c..adf37bc5d8a 100644 --- a/src/Aspire.Dashboard/Components/Layout/MobileNavMenu.razor +++ b/src/Aspire.Dashboard/Components/Layout/MobileNavMenu.razor @@ -41,6 +41,9 @@ [Parameter, EditorRequired] public required Func LaunchAIAssistantAsync { get; set; } + [Parameter, EditorRequired] + public required Func LaunchNotificationsAsync { get; set; } + [Parameter, EditorRequired] public required Func LaunchSettingsAsync { get; set; } } diff --git a/src/Aspire.Dashboard/Components/Layout/MobileNavMenu.razor.cs b/src/Aspire.Dashboard/Components/Layout/MobileNavMenu.razor.cs index 42ab3f27090..8809a238d59 100644 --- a/src/Aspire.Dashboard/Components/Layout/MobileNavMenu.razor.cs +++ b/src/Aspire.Dashboard/Components/Layout/MobileNavMenu.razor.cs @@ -98,6 +98,12 @@ private IEnumerable GetMobileNavMenuEntries() ); } + yield return new MobileNavMenuEntry( + Loc[nameof(Resources.Layout.MainLayoutLaunchNotifications)], + LaunchNotificationsAsync, + new Icons.Regular.Size24.Alert() + ); + yield return new MobileNavMenuEntry( Loc[nameof(Resources.Layout.MainLayoutLaunchSettings)], LaunchSettingsAsync, diff --git a/src/Aspire.Dashboard/Components/Layout/NotificationsHeaderButton.razor b/src/Aspire.Dashboard/Components/Layout/NotificationsHeaderButton.razor new file mode 100644 index 00000000000..e04bb5d1296 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Layout/NotificationsHeaderButton.razor @@ -0,0 +1,28 @@ +@using Aspire.Dashboard.Resources + +@if (UnreadCount > 0) +{ + + + + + + + +} +else +{ + + + +} diff --git a/src/Aspire.Dashboard/Components/Layout/NotificationsHeaderButton.razor.cs b/src/Aspire.Dashboard/Components/Layout/NotificationsHeaderButton.razor.cs new file mode 100644 index 00000000000..c44f82955b3 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Layout/NotificationsHeaderButton.razor.cs @@ -0,0 +1,42 @@ +// 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 Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; + +namespace Aspire.Dashboard.Components.Layout; + +public partial class NotificationsHeaderButton : ComponentBase, IDisposable +{ + [Parameter, EditorRequired] + public required Func OnClick { get; set; } + + [Inject] + public required INotificationService NotificationService { get; init; } + + [Inject] + public required IStringLocalizer Loc { get; init; } + + protected override void OnInitialized() + { + NotificationService.OnChange += HandleNotificationsChanged; + } + + private int UnreadCount => NotificationService.UnreadCount; + + private async Task HandleClick() + { + await OnClick(); + } + + private void HandleNotificationsChanged() + { + _ = InvokeAsync(StateHasChanged); + } + + public void Dispose() + { + NotificationService.OnChange -= HandleNotificationsChanged; + } +} diff --git a/src/Aspire.Dashboard/DashboardWebApplication.cs b/src/Aspire.Dashboard/DashboardWebApplication.cs index c2f0539fe85..d80e031cd98 100644 --- a/src/Aspire.Dashboard/DashboardWebApplication.cs +++ b/src/Aspire.Dashboard/DashboardWebApplication.cs @@ -270,6 +270,8 @@ public DashboardWebApplication( // Data from the server. builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(TimeProvider.System); builder.Services.TryAddScoped(); builder.Services.AddSingleton(); diff --git a/src/Aspire.Dashboard/Model/DashboardCommandExecutor.cs b/src/Aspire.Dashboard/Model/DashboardCommandExecutor.cs index 3eaf9a60e04..1273e86f56f 100644 --- a/src/Aspire.Dashboard/Model/DashboardCommandExecutor.cs +++ b/src/Aspire.Dashboard/Model/DashboardCommandExecutor.cs @@ -18,7 +18,8 @@ public sealed class DashboardCommandExecutor( IToastService toastService, IStringLocalizer loc, NavigationManager navigationManager, - DashboardTelemetryService telemetryService) + DashboardTelemetryService telemetryService, + INotificationService notificationService) { private readonly HashSet<(string ResourceName, string CommandName)> _executingCommands = []; private readonly object _lock = new object(); @@ -95,7 +96,15 @@ public async Task ExecuteAsyncCore(ResourceViewModel resource, CommandViewModel } } - var messageResourceName = getResourceName(resource); + var messageBarStartingTitle = string.Format(CultureInfo.InvariantCulture, loc[nameof(Dashboard.Resources.Resources.ResourceCommandStarting)], command.GetDisplayName()); + var toastStartingTitle = $"{getResourceName(resource)} {messageBarStartingTitle}"; + + // Add a notification to the notification center for the in-progress command. + var progressNotificationId = notificationService.AddNotification(new NotificationEntry + { + Title = messageBarStartingTitle, + Intent = MessageIntent.Info, + }); // When a resource command starts a toast is immediately shown. // The toast is open for a certain amount of time and then automatically closed. @@ -106,7 +115,7 @@ public async Task ExecuteAsyncCore(ResourceViewModel resource, CommandViewModel { Id = Guid.NewGuid().ToString(), Intent = ToastIntent.Progress, - Title = string.Format(CultureInfo.InvariantCulture, loc[nameof(Dashboard.Resources.Resources.ResourceCommandStarting)], messageResourceName, command.GetDisplayName()), + Title = toastStartingTitle, Content = new CommunicationToastContent(), Timeout = 0 // App logic will handle closing the toast }; @@ -122,14 +131,16 @@ public async Task ExecuteAsyncCore(ResourceViewModel resource, CommandViewModel }; ResourceCommandResponseViewModel response; - CancellationTokenSource closeToastCts; + // The CTS intentionally outlives the command execution to ensure we can close the toast in all scenarios + // e.g., even if the command execution fails or the toast is still open when the command finishes. + // It's ok to let it be cleaned up by GC when the short CancelAfter completes. + var closeToastCts = new CancellationTokenSource(); try { toastService.OnClose += closeCallback; // Show a toast immediately to indicate the command is starting. toastService.ShowCommunicationToast(toastParameters); - closeToastCts = new CancellationTokenSource(); closeToastCts.Token.Register(() => { toastService.CloseToast(toastParameters.Id); @@ -143,42 +154,73 @@ public async Task ExecuteAsyncCore(ResourceViewModel resource, CommandViewModel toastService.OnClose -= closeCallback; } - // Update toast with the result; + // Update toast and notification with the result. if (response.Kind == ResourceCommandResponseKind.Succeeded) { - toastParameters.Title = string.Format(CultureInfo.InvariantCulture, loc[nameof(Dashboard.Resources.Resources.ResourceCommandSuccess)], messageResourceName, command.GetDisplayName()); + var successTitle = string.Format(CultureInfo.InvariantCulture, loc[nameof(Dashboard.Resources.Resources.ResourceCommandSuccess)], command.GetDisplayName()); + toastParameters.Title = $"{getResourceName(resource)} {successTitle}"; toastParameters.Intent = ToastIntent.Success; toastParameters.Icon = GetIntentIcon(ToastIntent.Success); + + if (response.Result is not null) + { + toastParameters.PrimaryAction = loc[nameof(Dashboard.Resources.Resources.ResourceCommandViewResponse)]; + toastParameters.OnPrimaryAction = EventCallback.Factory.Create(this, () => OpenViewResponseDialogAsync(command, response)); + } + + notificationService.ReplaceNotification(progressNotificationId, new NotificationEntry + { + Title = successTitle, + Body = response.Message, + Intent = MessageIntent.Success, + PrimaryAction = response.Result is not null ? CreateViewResponseNotificationAction(command, response) : null + }); + + if (response.Result?.DisplayImmediately == true) + { + await OpenViewResponseDialogAsync(command, response).ConfigureAwait(false); + } } else if (response.Kind == ResourceCommandResponseKind.Cancelled) { - // For cancelled commands, just close the existing toast and don't show any success or error message + // For cancelled commands, just close the existing toast and don't show any success or error message. if (!toastClosed) { toastService.CloseToast(toastParameters.Id); } + + notificationService.RemoveNotification(progressNotificationId); + closeToastCts.Dispose(); return; } else { - toastParameters.Title = string.Format(CultureInfo.InvariantCulture, loc[nameof(Dashboard.Resources.Resources.ResourceCommandFailed)], messageResourceName, command.GetDisplayName()); + var failedTitle = string.Format(CultureInfo.InvariantCulture, loc[nameof(Dashboard.Resources.Resources.ResourceCommandFailed)], command.GetDisplayName()); + toastParameters.Title = $"{getResourceName(resource)} {failedTitle}"; toastParameters.Intent = ToastIntent.Error; toastParameters.Icon = GetIntentIcon(ToastIntent.Error); - toastParameters.Content.Details = response.ErrorMessage; toastParameters.PrimaryAction = loc[nameof(Dashboard.Resources.Resources.ResourceCommandToastViewLogs)]; toastParameters.OnPrimaryAction = EventCallback.Factory.Create(this, () => navigationManager.NavigateTo(DashboardUrls.ConsoleLogsUrl(resource: getResourceName(resource)))); - } + toastParameters.Content.Details = response.Message; - if (response.Result is not null) - { - var fixedFormat = response.ResultFormat == CommandResultFormat.Json ? DashboardUIHelpers.JsonFormat : null; - await TextVisualizerDialog.OpenDialogAsync(new OpenTextVisualizerDialogOptions + if (response.Result is not null) + { + toastParameters.SecondaryAction = loc[nameof(Dashboard.Resources.Resources.ResourceCommandViewResponse)]; + toastParameters.OnSecondaryAction = EventCallback.Factory.Create(this, () => OpenViewResponseDialogAsync(command, response)); + } + + notificationService.ReplaceNotification(progressNotificationId, new NotificationEntry { - DialogService = dialogService, - ValueDescription = command.GetDisplayName(), - Value = response.Result, - FixedFormat = fixedFormat - }).ConfigureAwait(false); + Title = failedTitle, + Body = response.Message, + Intent = MessageIntent.Error, + PrimaryAction = response.Result is not null ? CreateViewResponseNotificationAction(command, response) : null + }); + + if (response.Result?.DisplayImmediately == true) + { + await OpenViewResponseDialogAsync(command, response).ConfigureAwait(false); + } } if (!toastClosed) @@ -195,6 +237,8 @@ await TextVisualizerDialog.OpenDialogAsync(new OpenTextVisualizerDialogOptions // Show toast to display result. toastService.ShowCommunicationToast(toastParameters); + + closeToastCts.Dispose(); } } @@ -216,4 +260,31 @@ private static (Icon Icon, Color Color)? GetIntentIcon(ToastIntent intent) _ => throw new InvalidOperationException() }; } + + private NotificationAction CreateViewResponseNotificationAction(CommandViewModel command, ResourceCommandResponseViewModel response) + { + return new NotificationAction + { + Text = loc[nameof(Dashboard.Resources.Resources.ResourceCommandViewResponse)], + OnClick = () => OpenViewResponseDialogAsync(command, response) + }; + } + + private async Task OpenViewResponseDialogAsync(CommandViewModel command, ResourceCommandResponseViewModel response) + { + var fixedFormat = response.Result!.Format switch + { + CommandResultFormat.Json => DashboardUIHelpers.JsonFormat, + CommandResultFormat.Markdown => DashboardUIHelpers.MarkdownFormat, + _ => null + }; + + await TextVisualizerDialog.OpenDialogAsync(new OpenTextVisualizerDialogOptions + { + DialogService = dialogService, + ValueDescription = command.GetDisplayName(), + Value = response.Result.Value, + FixedFormat = fixedFormat + }).ConfigureAwait(false); + } } diff --git a/src/Aspire.Dashboard/Model/INotificationService.cs b/src/Aspire.Dashboard/Model/INotificationService.cs new file mode 100644 index 00000000000..408b7d589df --- /dev/null +++ b/src/Aspire.Dashboard/Model/INotificationService.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.FluentUI.AspNetCore.Components; + +namespace Aspire.Dashboard.Model; + +/// +/// Stores notifications for the dashboard notification center. +/// Implementations must be thread-safe as the service is registered as a singleton +/// and accessed from multiple Blazor circuits. +/// +public interface INotificationService +{ + /// + /// Gets the number of notifications added since the dialog was last opened. + /// + int UnreadCount { get; } + + /// + /// Gets a snapshot of the current notifications, most recent first. + /// + IReadOnlyList GetNotifications(); + + /// + /// Adds a notification and raises . + /// + /// The ID of the added notification, which can be used to replace it later. + string AddNotification(NotificationEntry notification); + + /// + /// Replaces an existing notification (matched by ) and raises . + /// + void ReplaceNotification(string id, NotificationEntry notification); + + /// + /// Removes a notification by ID and raises . + /// + void RemoveNotification(string id); + + /// + /// Removes all notifications and raises . + /// + void ClearAll(); + + /// + /// Resets the unread count to zero and raises . + /// + void ResetUnreadCount(); + + /// + /// Raised when notifications or the unread count change. + /// + event Action? OnChange; +} + +/// +/// Represents a single notification in the notification center. +/// +public sealed class NotificationEntry +{ + public required string Title { get; init; } + public string? Body { get; init; } + public required MessageIntent Intent { get; init; } + public DateTimeOffset Timestamp { get; set; } + public NotificationAction? PrimaryAction { get; init; } +} + +/// +/// An action button displayed on a notification. +/// +public sealed class NotificationAction +{ + public required string Text { get; init; } + public required Func OnClick { get; init; } +} + +/// +/// A notification with its service-assigned ID. +/// +public sealed class NotificationMessage +{ + public required string Id { get; init; } + public required NotificationEntry Entry { get; init; } +} diff --git a/src/Aspire.Dashboard/Model/NotificationService.cs b/src/Aspire.Dashboard/Model/NotificationService.cs new file mode 100644 index 00000000000..0c96c4ff00d --- /dev/null +++ b/src/Aspire.Dashboard/Model/NotificationService.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Dashboard.Model; + +/// +/// Thread-safe singleton implementation of . +/// +internal sealed class NotificationService(TimeProvider timeProvider) : INotificationService +{ + private const int MaxNotifications = 100; + + private readonly object _lock = new(); + private readonly List<(string Id, NotificationEntry Entry)> _notifications = []; + private int _unreadCount; + + public int UnreadCount + { + get + { + lock (_lock) + { + return _unreadCount; + } + } + } + + public event Action? OnChange; + + public IReadOnlyList GetNotifications() + { + lock (_lock) + { + // Return a snapshot, most recent first. + var result = new NotificationMessage[_notifications.Count]; + for (var i = 0; i < _notifications.Count; i++) + { + var item = _notifications[_notifications.Count - 1 - i]; + result[i] = new NotificationMessage { Id = item.Id, Entry = item.Entry }; + } + + return result; + } + } + + public string AddNotification(NotificationEntry notification) + { + notification.Timestamp = timeProvider.GetUtcNow(); + var id = Guid.NewGuid().ToString("N"); + lock (_lock) + { + _notifications.Add((id, notification)); + _unreadCount++; + + // Remove oldest notifications when the limit is exceeded. + while (_notifications.Count > MaxNotifications) + { + _notifications.RemoveAt(0); + } + } + + OnChange?.Invoke(); + return id; + } + + public void ReplaceNotification(string id, NotificationEntry notification) + { + notification.Timestamp = timeProvider.GetUtcNow(); + lock (_lock) + { + for (var i = 0; i < _notifications.Count; i++) + { + if (_notifications[i].Id == id) + { + _notifications[i] = (id, notification); + break; + } + } + } + + OnChange?.Invoke(); + } + + public void RemoveNotification(string id) + { + lock (_lock) + { + _notifications.RemoveAll(n => n.Id == id); + } + + OnChange?.Invoke(); + } + + public void ClearAll() + { + lock (_lock) + { + _notifications.Clear(); + _unreadCount = 0; + } + + OnChange?.Invoke(); + } + + public void ResetUnreadCount() + { + lock (_lock) + { + _unreadCount = 0; + } + + OnChange?.Invoke(); + } +} diff --git a/src/Aspire.Dashboard/Model/ResourceCommandResponseViewModel.cs b/src/Aspire.Dashboard/Model/ResourceCommandResponseViewModel.cs index c13f6777a64..fa7be2f159a 100644 --- a/src/Aspire.Dashboard/Model/ResourceCommandResponseViewModel.cs +++ b/src/Aspire.Dashboard/Model/ResourceCommandResponseViewModel.cs @@ -7,8 +7,18 @@ public class ResourceCommandResponseViewModel { public required ResourceCommandResponseKind Kind { get; init; } public string? ErrorMessage { get; init; } - public string? Result { get; init; } - public CommandResultFormat? ResultFormat { get; init; } + public string? Message { get; init; } + public ResourceCommandResultViewModel? Result { get; init; } +} + +/// +/// Represents a value produced by a command. +/// +public class ResourceCommandResultViewModel +{ + public required string Value { get; init; } + public CommandResultFormat Format { get; init; } + public bool DisplayImmediately { get; init; } } // Must be kept in sync with ResourceCommandResponseKind in the resource_service.proto file @@ -33,5 +43,10 @@ public enum CommandResultFormat /// /// JSON result. /// - Json + Json, + + /// + /// Markdown result. + /// + Markdown } diff --git a/src/Aspire.Dashboard/Model/TextVisualizerViewModel.cs b/src/Aspire.Dashboard/Model/TextVisualizerViewModel.cs index 7b54614af07..884b835a9a4 100644 --- a/src/Aspire.Dashboard/Model/TextVisualizerViewModel.cs +++ b/src/Aspire.Dashboard/Model/TextVisualizerViewModel.cs @@ -271,6 +271,10 @@ internal void UpdateFormat(string newFormat) ChangeFormattedText(newFormat, formattedJson); } } + else if (newFormat == DashboardUIHelpers.MarkdownFormat) + { + ChangeFormattedText(newFormat, Text); + } else { ChangeFormattedText(DashboardUIHelpers.PlaintextFormat, Text); diff --git a/src/Aspire.Dashboard/Resources/Dialogs.Designer.cs b/src/Aspire.Dashboard/Resources/Dialogs.Designer.cs index 3ad4c1d03a7..5ce37e103e9 100644 --- a/src/Aspire.Dashboard/Resources/Dialogs.Designer.cs +++ b/src/Aspire.Dashboard/Resources/Dialogs.Designer.cs @@ -1060,7 +1060,7 @@ public static string SettingsRemoveAllButtonText { } /// - /// Looks up a localized string similar to Format JSON. + /// Looks up a localized string similar to JSON. /// public static string TextVisualizerDialogJsonFormat { get { @@ -1068,6 +1068,15 @@ public static string TextVisualizerDialogJsonFormat { } } + /// + /// Looks up a localized string similar to Markdown. + /// + public static string TextVisualizerDialogMarkdownFormat { + get { + return ResourceManager.GetString("TextVisualizerDialogMarkdownFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unformatted. /// @@ -1078,7 +1087,7 @@ public static string TextVisualizerDialogPlaintextFormat { } /// - /// Looks up a localized string similar to Format XML. + /// Looks up a localized string similar to XML. /// public static string TextVisualizerDialogXmlFormat { get { @@ -1157,5 +1166,32 @@ public static string SettingsDialogTimeFormatTwentyFourHour { return ResourceManager.GetString("SettingsDialogTimeFormatTwentyFourHour", resourceCulture); } } + + /// + /// Looks up a localized string similar to Dismiss all. + /// + public static string NotificationCenterDismissAll { + get { + return ResourceManager.GetString("NotificationCenterDismissAll", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No notifications. + /// + public static string NotificationCenterEmpty { + get { + return ResourceManager.GetString("NotificationCenterEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dismiss notification. + /// + public static string NotificationEntryDismiss { + get { + return ResourceManager.GetString("NotificationEntryDismiss", resourceCulture); + } + } } } diff --git a/src/Aspire.Dashboard/Resources/Dialogs.resx b/src/Aspire.Dashboard/Resources/Dialogs.resx index 0bde462b9ba..7ab69db6224 100644 --- a/src/Aspire.Dashboard/Resources/Dialogs.resx +++ b/src/Aspire.Dashboard/Resources/Dialogs.resx @@ -246,10 +246,13 @@ Unformatted - Format JSON + JSON - Format XML + XML + + + Markdown Select format @@ -492,4 +495,13 @@ 24-hour + + Dismiss all + + + No notifications + + + Dismiss notification + diff --git a/src/Aspire.Dashboard/Resources/Layout.Designer.cs b/src/Aspire.Dashboard/Resources/Layout.Designer.cs index 5cf06ad088d..bf5bb9d75c8 100644 --- a/src/Aspire.Dashboard/Resources/Layout.Designer.cs +++ b/src/Aspire.Dashboard/Resources/Layout.Designer.cs @@ -96,6 +96,24 @@ public static string MainLayoutLaunchSettings { } } + /// + /// Looks up a localized string similar to Notifications. + /// + public static string MainLayoutLaunchNotifications { + get { + return ResourceManager.GetString("MainLayoutLaunchNotifications", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Notifications. + /// + public static string MainLayoutNotificationCenterTitle { + get { + return ResourceManager.GetString("MainLayoutNotificationCenterTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Close. /// diff --git a/src/Aspire.Dashboard/Resources/Layout.resx b/src/Aspire.Dashboard/Resources/Layout.resx index 265a0431459..813b1b006b7 100644 --- a/src/Aspire.Dashboard/Resources/Layout.resx +++ b/src/Aspire.Dashboard/Resources/Layout.resx @@ -138,6 +138,12 @@ Close + + Notifications + + + Notifications + Resources diff --git a/src/Aspire.Dashboard/Resources/Resources.Designer.cs b/src/Aspire.Dashboard/Resources/Resources.Designer.cs index 8bc44068006..8ef9fa6c1c3 100644 --- a/src/Aspire.Dashboard/Resources/Resources.Designer.cs +++ b/src/Aspire.Dashboard/Resources/Resources.Designer.cs @@ -124,7 +124,7 @@ public static string ResourceCollapseAllChildren { } /// - /// Looks up a localized string similar to {0} "{1}" failed. + /// Looks up a localized string similar to "{0}" failed. /// public static string ResourceCommandFailed { get { @@ -133,7 +133,7 @@ public static string ResourceCommandFailed { } /// - /// Looks up a localized string similar to {0} "{1}" starting. + /// Looks up a localized string similar to "{0}" starting. /// public static string ResourceCommandStarting { get { @@ -142,7 +142,7 @@ public static string ResourceCommandStarting { } /// - /// Looks up a localized string similar to {0} "{1}" succeeded. + /// Looks up a localized string similar to "{0}" succeeded. /// public static string ResourceCommandSuccess { get { @@ -159,6 +159,15 @@ public static string ResourceCommandToastViewLogs { } } + /// + /// Looks up a localized string similar to View response. + /// + public static string ResourceCommandViewResponse { + get { + return ResourceManager.GetString("ResourceCommandViewResponse", resourceCulture); + } + } + /// /// Looks up a localized string similar to View console logs. /// diff --git a/src/Aspire.Dashboard/Resources/Resources.resx b/src/Aspire.Dashboard/Resources/Resources.resx index 3825dd1a9c2..723933e4d49 100644 --- a/src/Aspire.Dashboard/Resources/Resources.resx +++ b/src/Aspire.Dashboard/Resources/Resources.resx @@ -209,16 +209,19 @@ State - {0} "{1}" failed - {0} is the resource. {1} is the display name of the command. + "{0}" failed + {0} is the display name of the command. - {0} "{1}" succeeded - {0} is the resource. {1} is the display name of the command. + "{0}" succeeded + {0} is the display name of the command. View console logs + + View response + Actions @@ -238,8 +241,8 @@ Stop time - {0} "{1}" starting - {0} is the resource. {1} is the display name of the command. + "{0}" starting + {0} is the display name of the command. Traces diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.cs.xlf index 4abedf4c8cd..d7a7aff41b1 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.cs.xlf @@ -457,6 +457,21 @@ Trasování + + Dismiss all + Dismiss all + + + + No notifications + No notifications + + + + Dismiss notification + Dismiss notification + + Open in text visualizer Otevřít ve vizualizéru textu @@ -578,8 +593,13 @@ - Format JSON - Formátovat JSON + JSON + Formátovat JSON + + + + Markdown + Markdown @@ -588,8 +608,8 @@ - Format XML - Formátovat XML + XML + Formátovat XML diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.de.xlf index 9edb168258e..dba95e3abdc 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.de.xlf @@ -457,6 +457,21 @@ Ablaufverfolgungen + + Dismiss all + Dismiss all + + + + No notifications + No notifications + + + + Dismiss notification + Dismiss notification + + Open in text visualizer In Textschnellansicht öffnen @@ -578,8 +593,13 @@ - Format JSON - JSON formatieren + JSON + JSON formatieren + + + + Markdown + Markdown @@ -588,8 +608,8 @@ - Format XML - XML formatieren + XML + XML formatieren diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.es.xlf index 67c286a55ee..f3cedddf742 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.es.xlf @@ -457,6 +457,21 @@ Seguimientos + + Dismiss all + Dismiss all + + + + No notifications + No notifications + + + + Dismiss notification + Dismiss notification + + Open in text visualizer Abrir en visualizador de texto @@ -578,8 +593,13 @@ - Format JSON - Formato JSON + JSON + Formato JSON + + + + Markdown + Markdown @@ -588,8 +608,8 @@ - Format XML - Formato XML + XML + Formato XML diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.fr.xlf index b4d45da519a..8b87eaaad42 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.fr.xlf @@ -457,6 +457,21 @@ Traces + + Dismiss all + Dismiss all + + + + No notifications + No notifications + + + + Dismiss notification + Dismiss notification + + Open in text visualizer Ouvrir dans le visualiseur de texte @@ -578,8 +593,13 @@ - Format JSON - Format JSON + JSON + Format JSON + + + + Markdown + Markdown @@ -588,8 +608,8 @@ - Format XML - Format XML + XML + Format XML diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.it.xlf index bbd12577f8c..0768ed925d6 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.it.xlf @@ -457,6 +457,21 @@ Tracce + + Dismiss all + Dismiss all + + + + No notifications + No notifications + + + + Dismiss notification + Dismiss notification + + Open in text visualizer Apri nel visualizzatore di testo @@ -578,8 +593,13 @@ - Format JSON - Formato JSON + JSON + Formato JSON + + + + Markdown + Markdown @@ -588,8 +608,8 @@ - Format XML - Formatta XML + XML + Formatta XML diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ja.xlf index c98700bc8c5..5be49390e46 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ja.xlf @@ -457,6 +457,21 @@ トレース + + Dismiss all + Dismiss all + + + + No notifications + No notifications + + + + Dismiss notification + Dismiss notification + + Open in text visualizer テキスト ビジュアライザーで開く @@ -578,8 +593,13 @@ - Format JSON - JSON の書式設定 + JSON + JSON の書式設定 + + + + Markdown + Markdown @@ -588,8 +608,8 @@ - Format XML - XML の書式設定 + XML + XML の書式設定 diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ko.xlf index 4005607a1d0..db374a04fdf 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ko.xlf @@ -457,6 +457,21 @@ 추적 + + Dismiss all + Dismiss all + + + + No notifications + No notifications + + + + Dismiss notification + Dismiss notification + + Open in text visualizer 텍스트 시각화 도우미에서 열기 @@ -578,8 +593,13 @@ - Format JSON - JSON 형식 지정 + JSON + JSON 형식 지정 + + + + Markdown + Markdown @@ -588,8 +608,8 @@ - Format XML - XML 형식 지정 + XML + XML 형식 지정 diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.pl.xlf index 51968696c63..f46dea5443f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.pl.xlf @@ -457,6 +457,21 @@ Ślady + + Dismiss all + Dismiss all + + + + No notifications + No notifications + + + + Dismiss notification + Dismiss notification + + Open in text visualizer Otwórz w wizualizatorze tekstu @@ -578,8 +593,13 @@ - Format JSON - Formatuj kod JSON + JSON + Formatuj kod JSON + + + + Markdown + Markdown @@ -588,8 +608,8 @@ - Format XML - Formatuj kod XML + XML + Formatuj kod XML diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.pt-BR.xlf index efcdf07c74b..6813bd6d45c 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.pt-BR.xlf @@ -457,6 +457,21 @@ Rastreamentos + + Dismiss all + Dismiss all + + + + No notifications + No notifications + + + + Dismiss notification + Dismiss notification + + Open in text visualizer Abrir no visualizador de texto @@ -578,8 +593,13 @@ - Format JSON - Formatar JSON + JSON + Formatar JSON + + + + Markdown + Markdown @@ -588,8 +608,8 @@ - Format XML - Formatar XML + XML + Formatar XML diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ru.xlf index 815849031c2..664204a1d21 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ru.xlf @@ -457,6 +457,21 @@ Трассировки + + Dismiss all + Dismiss all + + + + No notifications + No notifications + + + + Dismiss notification + Dismiss notification + + Open in text visualizer Открыть в визуализаторе текста @@ -578,8 +593,13 @@ - Format JSON - Формат JSON + JSON + Формат JSON + + + + Markdown + Markdown @@ -588,8 +608,8 @@ - Format XML - Формат XML + XML + Формат XML diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.tr.xlf index 317a66232de..db5fec1cdcd 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.tr.xlf @@ -457,6 +457,21 @@ İzlemeler + + Dismiss all + Dismiss all + + + + No notifications + No notifications + + + + Dismiss notification + Dismiss notification + + Open in text visualizer Metin görselleştiricide aç @@ -578,8 +593,13 @@ - Format JSON - JSON dosyasını biçimlendir + JSON + JSON dosyasını biçimlendir + + + + Markdown + Markdown @@ -588,8 +608,8 @@ - Format XML - XML dosyasını biçimlendir + XML + XML dosyasını biçimlendir diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hans.xlf index 192ce5dddd8..eef859d147a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hans.xlf @@ -457,6 +457,21 @@ 跟踪 + + Dismiss all + Dismiss all + + + + No notifications + No notifications + + + + Dismiss notification + Dismiss notification + + Open in text visualizer 在文本可视化工具中打开 @@ -578,8 +593,13 @@ - Format JSON - 格式化 JSON + JSON + 格式化 JSON + + + + Markdown + Markdown @@ -588,8 +608,8 @@ - Format XML - 格式化 XML + XML + 格式化 XML diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hant.xlf index 3f53eb074ae..f8426dd5996 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hant.xlf @@ -457,6 +457,21 @@ 追蹤 + + Dismiss all + Dismiss all + + + + No notifications + No notifications + + + + Dismiss notification + Dismiss notification + + Open in text visualizer 在文字視覺化工具中開啟 @@ -578,8 +593,13 @@ - Format JSON - 格式化 JSON + JSON + 格式化 JSON + + + + Markdown + Markdown @@ -588,8 +608,8 @@ - Format XML - 格式化 XML + XML + 格式化 XML diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.cs.xlf index e8b30000aaa..61b7c938052 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.cs.xlf @@ -17,11 +17,21 @@ Úložiště Aspire + + Notifications + Notifications + + Settings Nastavení + + Notifications + Notifications + + Close Zavřít diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.de.xlf index dcf7ebd332b..6233aef3419 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.de.xlf @@ -17,11 +17,21 @@ Aspire-Repository + + Notifications + Notifications + + Settings Einstellungen + + Notifications + Notifications + + Close Schließen diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.es.xlf index a7b4dc66cdf..fe567fc07c3 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.es.xlf @@ -17,11 +17,21 @@ Repositorio de Aspire + + Notifications + Notifications + + Settings Configuración + + Notifications + Notifications + + Close Cerrar diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.fr.xlf index 14ce1089dac..e4398dd3a1e 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.fr.xlf @@ -17,11 +17,21 @@ Référentiel Aspire + + Notifications + Notifications + + Settings Paramètres + + Notifications + Notifications + + Close Fermer diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.it.xlf index 92797b80416..81ca6eb72d6 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.it.xlf @@ -17,11 +17,21 @@ Repository Aspire + + Notifications + Notifications + + Settings Impostazioni + + Notifications + Notifications + + Close Chiudi diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.ja.xlf index 690f394c922..154b7c4bc19 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.ja.xlf @@ -17,11 +17,21 @@ Aspire リポジトリ + + Notifications + Notifications + + Settings 設定 + + Notifications + Notifications + + Close 閉じる diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.ko.xlf index d4a3445aea8..4bf337c211d 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.ko.xlf @@ -17,11 +17,21 @@ Aspire 리포지토리 + + Notifications + Notifications + + Settings 설정 + + Notifications + Notifications + + Close 닫기 diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.pl.xlf index 4e55919bd9b..cd628d961ac 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.pl.xlf @@ -17,11 +17,21 @@ Repozytorium Aspire + + Notifications + Notifications + + Settings Ustawienia + + Notifications + Notifications + + Close Zamknij diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.pt-BR.xlf index e9b9a33a638..ee3f0ec8426 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.pt-BR.xlf @@ -17,11 +17,21 @@ Repositório do Aspire + + Notifications + Notifications + + Settings Configurações + + Notifications + Notifications + + Close Fechar diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.ru.xlf index f16dde28c02..23b72d896e0 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.ru.xlf @@ -17,11 +17,21 @@ Репозиторий Aspire + + Notifications + Notifications + + Settings Параметры + + Notifications + Notifications + + Close Закрыть diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.tr.xlf index 6b9931a8c37..81862c837ff 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.tr.xlf @@ -17,11 +17,21 @@ Aspire deposu + + Notifications + Notifications + + Settings Ayarlar + + Notifications + Notifications + + Close Kapat diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hans.xlf index 5cb24e62d08..5fe074192c2 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hans.xlf @@ -17,11 +17,21 @@ Aspire 存储库 + + Notifications + Notifications + + Settings 设置 + + Notifications + Notifications + + Close 关闭 diff --git a/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hant.xlf index 0228089de12..07e9e07822b 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Layout.zh-Hant.xlf @@ -17,11 +17,21 @@ Aspire 存放庫 + + Notifications + Notifications + + Settings 設定 + + Notifications + Notifications + + Close 關閉 diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.cs.xlf index 2564b659d52..2e5c37eab2c 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.cs.xlf @@ -53,25 +53,30 @@ - {0} "{1}" failed - Selhalo: {0} ({1}) - {0} is the resource. {1} is the display name of the command. + "{0}" failed + "{0}" failed + {0} is the display name of the command. - {0} "{1}" starting - Spouští se {0} ({1}) - {0} is the resource. {1} is the display name of the command. + "{0}" starting + "{0}" starting + {0} is the display name of the command. - {0} "{1}" succeeded - Úspěšné: {0} ({1}) - {0} is the resource. {1} is the display name of the command. + "{0}" succeeded + "{0}" succeeded + {0} is the display name of the command. View console logs Zobrazit protokoly konzoly + + View response + View response + + View console logs Zobrazit protokoly konzoly diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.de.xlf index 752e0d1a0e9..82a3cb1c3f1 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.de.xlf @@ -53,25 +53,30 @@ - {0} "{1}" failed - {0} "{1}" fehlgeschlagen - {0} is the resource. {1} is the display name of the command. + "{0}" failed + "{0}" failed + {0} is the display name of the command. - {0} "{1}" starting - {0} "{1}" wird gestartet - {0} is the resource. {1} is the display name of the command. + "{0}" starting + "{0}" starting + {0} is the display name of the command. - {0} "{1}" succeeded - {0} "{1}" erfolgreich - {0} is the resource. {1} is the display name of the command. + "{0}" succeeded + "{0}" succeeded + {0} is the display name of the command. View console logs Konsolenprotokolle anzeigen + + View response + View response + + View console logs Konsolenprotokolle anzeigen diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.es.xlf index c0a78581ad1..623481c7dd6 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.es.xlf @@ -53,25 +53,30 @@ - {0} "{1}" failed - {0} "{1}" erróneo - {0} is the resource. {1} is the display name of the command. + "{0}" failed + "{0}" failed + {0} is the display name of the command. - {0} "{1}" starting - {0} "{1}" iniciando - {0} is the resource. {1} is the display name of the command. + "{0}" starting + "{0}" starting + {0} is the display name of the command. - {0} "{1}" succeeded - {0} "{1}" correcto - {0} is the resource. {1} is the display name of the command. + "{0}" succeeded + "{0}" succeeded + {0} is the display name of the command. View console logs Ver registros de consola + + View response + View response + + View console logs Ver registros de consola diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.fr.xlf index 805a1ae46ec..c70490f81c6 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.fr.xlf @@ -53,25 +53,30 @@ - {0} "{1}" failed - {0} « {1} » a échoué - {0} is the resource. {1} is the display name of the command. + "{0}" failed + "{0}" failed + {0} is the display name of the command. - {0} "{1}" starting - Démarrage de {0} « {1} » - {0} is the resource. {1} is the display name of the command. + "{0}" starting + "{0}" starting + {0} is the display name of the command. - {0} "{1}" succeeded - {0} « {1} » a réussi - {0} is the resource. {1} is the display name of the command. + "{0}" succeeded + "{0}" succeeded + {0} is the display name of the command. View console logs Afficher les journaux de console + + View response + View response + + View console logs Afficher les journaux de console diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.it.xlf index 6b1f7666d6c..8abba77b611 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.it.xlf @@ -53,25 +53,30 @@ - {0} "{1}" failed - {0} "{1}" non riuscito - {0} is the resource. {1} is the display name of the command. + "{0}" failed + "{0}" failed + {0} is the display name of the command. - {0} "{1}" starting - Avvio di{0} "{1}" in corso - {0} is the resource. {1} is the display name of the command. + "{0}" starting + "{0}" starting + {0} is the display name of the command. - {0} "{1}" succeeded - {0} "{1}" completato - {0} is the resource. {1} is the display name of the command. + "{0}" succeeded + "{0}" succeeded + {0} is the display name of the command. View console logs Visualizza log della console + + View response + View response + + View console logs Visualizza log della console diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.ja.xlf index 3456b81da1f..b89af93c589 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.ja.xlf @@ -53,25 +53,30 @@ - {0} "{1}" failed - {0} "{1}" が失敗しました - {0} is the resource. {1} is the display name of the command. + "{0}" failed + "{0}" failed + {0} is the display name of the command. - {0} "{1}" starting - {0} "{1}" を開始しています - {0} is the resource. {1} is the display name of the command. + "{0}" starting + "{0}" starting + {0} is the display name of the command. - {0} "{1}" succeeded - {0} "{1}" が成功しました - {0} is the resource. {1} is the display name of the command. + "{0}" succeeded + "{0}" succeeded + {0} is the display name of the command. View console logs コンソール ログの表示 + + View response + View response + + View console logs コンソール ログの表示 diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.ko.xlf index f28c867a848..d13be6da0e6 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.ko.xlf @@ -53,25 +53,30 @@ - {0} "{1}" failed - {0} "{1}" 실패함 - {0} is the resource. {1} is the display name of the command. + "{0}" failed + "{0}" failed + {0} is the display name of the command. - {0} "{1}" starting - {0} "{1}" 시작 중 - {0} is the resource. {1} is the display name of the command. + "{0}" starting + "{0}" starting + {0} is the display name of the command. - {0} "{1}" succeeded - {0} "{1}" 성공함 - {0} is the resource. {1} is the display name of the command. + "{0}" succeeded + "{0}" succeeded + {0} is the display name of the command. View console logs 콘솔 로그 보기 + + View response + View response + + View console logs 콘솔 로그 보기 diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.pl.xlf index d91aa66f20a..e380d066687 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.pl.xlf @@ -53,25 +53,30 @@ - {0} "{1}" failed - Niepowodzenie {0} „{1}” - {0} is the resource. {1} is the display name of the command. + "{0}" failed + "{0}" failed + {0} is the display name of the command. - {0} "{1}" starting - Uruchamianie {0} „{1}” - {0} is the resource. {1} is the display name of the command. + "{0}" starting + "{0}" starting + {0} is the display name of the command. - {0} "{1}" succeeded - Powodzenie {0} „{1}” - {0} is the resource. {1} is the display name of the command. + "{0}" succeeded + "{0}" succeeded + {0} is the display name of the command. View console logs Wyświetl dzienniki konsoli + + View response + View response + + View console logs Wyświetl dzienniki konsoli diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.pt-BR.xlf index 77cf66f3c3e..01b3cb8502c 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.pt-BR.xlf @@ -53,25 +53,30 @@ - {0} "{1}" failed - {0} "{1}" falhou - {0} is the resource. {1} is the display name of the command. + "{0}" failed + "{0}" failed + {0} is the display name of the command. - {0} "{1}" starting - {0} "{1}" iniciando - {0} is the resource. {1} is the display name of the command. + "{0}" starting + "{0}" starting + {0} is the display name of the command. - {0} "{1}" succeeded - {0} "{1}" bem-sucedido - {0} is the resource. {1} is the display name of the command. + "{0}" succeeded + "{0}" succeeded + {0} is the display name of the command. View console logs Exibir registros do console + + View response + View response + + View console logs Exibir registros do console diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.ru.xlf index 100eb0e9488..d73ffa63f05 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.ru.xlf @@ -53,25 +53,30 @@ - {0} "{1}" failed - Сбой {0} "{1}" - {0} is the resource. {1} is the display name of the command. + "{0}" failed + "{0}" failed + {0} is the display name of the command. - {0} "{1}" starting - Запуск {0} "{1}" - {0} is the resource. {1} is the display name of the command. + "{0}" starting + "{0}" starting + {0} is the display name of the command. - {0} "{1}" succeeded - {0} "{1}" успешно выполнено - {0} is the resource. {1} is the display name of the command. + "{0}" succeeded + "{0}" succeeded + {0} is the display name of the command. View console logs Просмотр журналов консоли + + View response + View response + + View console logs Просмотр журналов консоли diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.tr.xlf index 19ac2b6e988..3a196adc478 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.tr.xlf @@ -53,25 +53,30 @@ - {0} "{1}" failed - {0} "{1}" başarısız - {0} is the resource. {1} is the display name of the command. + "{0}" failed + "{0}" failed + {0} is the display name of the command. - {0} "{1}" starting - {0} "{1}" başlatılıyor - {0} is the resource. {1} is the display name of the command. + "{0}" starting + "{0}" starting + {0} is the display name of the command. - {0} "{1}" succeeded - {0} "{1}" başarılı - {0} is the resource. {1} is the display name of the command. + "{0}" succeeded + "{0}" succeeded + {0} is the display name of the command. View console logs Konsol günlüklerini görüntüle + + View response + View response + + View console logs Konsol günlüklerini görüntüle diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hans.xlf index d4fdd83c936..6d574d8340e 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hans.xlf @@ -53,25 +53,30 @@ - {0} "{1}" failed - {0} "{1}" 失败 - {0} is the resource. {1} is the display name of the command. + "{0}" failed + "{0}" failed + {0} is the display name of the command. - {0} "{1}" starting - {0} "{1}" 正在启动 - {0} is the resource. {1} is the display name of the command. + "{0}" starting + "{0}" starting + {0} is the display name of the command. - {0} "{1}" succeeded - {0} "{1}" 成功 - {0} is the resource. {1} is the display name of the command. + "{0}" succeeded + "{0}" succeeded + {0} is the display name of the command. View console logs 查看控制台日志 + + View response + View response + + View console logs 查看控制台日志 diff --git a/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hant.xlf index 70541f8f160..28273b63b64 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hant.xlf @@ -53,25 +53,30 @@ - {0} "{1}" failed - {0}「{1}」失敗 - {0} is the resource. {1} is the display name of the command. + "{0}" failed + "{0}" failed + {0} is the display name of the command. - {0} "{1}" starting - {0}「{1}」正在啟動 - {0} is the resource. {1} is the display name of the command. + "{0}" starting + "{0}" starting + {0} is the display name of the command. - {0} "{1}" succeeded - {0}「{1}」成功 - {0} is the resource. {1} is the display name of the command. + "{0}" succeeded + "{0}" succeeded + {0} is the display name of the command. View console logs 檢視主控台記錄 + + View response + View response + + View console logs 檢視主機記錄 diff --git a/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs b/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs index 7c045b0f48f..fcb05e46355 100644 --- a/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs +++ b/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs @@ -780,7 +780,8 @@ public async Task ExecuteResourceCommandAsync( return new ResourceCommandResponseViewModel() { Kind = ResourceCommandResponseKind.Failed, - ErrorMessage = errorMessage + ErrorMessage = errorMessage, + Message = errorMessage }; } } diff --git a/src/Aspire.Dashboard/ServiceClient/Partials.cs b/src/Aspire.Dashboard/ServiceClient/Partials.cs index 88c8418a0f0..94dade57cc9 100644 --- a/src/Aspire.Dashboard/ServiceClient/Partials.cs +++ b/src/Aspire.Dashboard/ServiceClient/Partials.cs @@ -196,17 +196,28 @@ partial class ResourceCommandResponse { public ResourceCommandResponseViewModel ToViewModel() { + // Map deprecated error_message to message for backward compatibility. +#pragma warning disable CS0612 // Type or member is obsolete + var resolvedMessage = HasMessage ? Message : ErrorMessage; +#pragma warning restore CS0612 // Type or member is obsolete + return new ResourceCommandResponseViewModel() { - ErrorMessage = ErrorMessage, + ErrorMessage = resolvedMessage, + Message = resolvedMessage, Kind = (Dashboard.Model.ResourceCommandResponseKind)Kind, - Result = HasResult ? Result : null, - ResultFormat = ResultFormat switch + Result = Result is not null ? new ResourceCommandResultViewModel { - CommandResultFormat.Text => Dashboard.Model.CommandResultFormat.Text, - CommandResultFormat.Json => Dashboard.Model.CommandResultFormat.Json, - _ => null - } + Value = Result.Value, + Format = Result.Format switch + { + CommandResultFormat.Text => Dashboard.Model.CommandResultFormat.Text, + CommandResultFormat.Json => Dashboard.Model.CommandResultFormat.Json, + CommandResultFormat.Markdown => Dashboard.Model.CommandResultFormat.Markdown, + _ => Dashboard.Model.CommandResultFormat.Text + }, + DisplayImmediately = Result.DisplayImmediately + } : null }; } } diff --git a/src/Aspire.Hosting/ApplicationModel/CommandsConfigurationExtensions.cs b/src/Aspire.Hosting/ApplicationModel/CommandsConfigurationExtensions.cs index bb27fd0b8f7..d2ec49098b2 100644 --- a/src/Aspire.Hosting/ApplicationModel/CommandsConfigurationExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/CommandsConfigurationExtensions.cs @@ -27,7 +27,7 @@ internal static void AddLifeCycleCommands(this IResource resource) var orchestrator = context.ServiceProvider.GetRequiredService(); await orchestrator.StartResourceAsync(context.ResourceName, context.CancellationToken).ConfigureAwait(false); - return CommandResults.Success(); + return new ExecuteCommandResult { Success = true, Message = string.Format(CultureInfo.InvariantCulture, CommandStrings.ResourceStarted, context.ResourceName) }; }, updateState: context => { @@ -60,7 +60,7 @@ internal static void AddLifeCycleCommands(this IResource resource) var orchestrator = context.ServiceProvider.GetRequiredService(); await orchestrator.StopResourceAsync(context.ResourceName, context.CancellationToken).ConfigureAwait(false); - return CommandResults.Success(); + return new ExecuteCommandResult { Success = true, Message = string.Format(CultureInfo.InvariantCulture, CommandStrings.ResourceStopped, context.ResourceName) }; }, updateState: context => { @@ -100,7 +100,7 @@ internal static void AddLifeCycleCommands(this IResource resource) await orchestrator.StopResourceAsync(context.ResourceName, context.CancellationToken).ConfigureAwait(false); await orchestrator.StartResourceAsync(context.ResourceName, context.CancellationToken).ConfigureAwait(false); - return CommandResults.Success(); + return new ExecuteCommandResult { Success = true, Message = string.Format(CultureInfo.InvariantCulture, CommandStrings.ResourceRestarted, context.ResourceName) }; }, updateState: context => { @@ -189,7 +189,7 @@ private static async Task ExecuteRebuildAsync(ExecuteComma var rebuilderResource = model.Resources.OfType().FirstOrDefault(r => r.Parent == projectResource); if (rebuilderResource is null) { - return new ExecuteCommandResult { Success = false, ErrorMessage = string.Format(CultureInfo.InvariantCulture, CommandStrings.RebuilderResourceNotFound, projectResource.Name) }; + return new ExecuteCommandResult { Success = false, Message = string.Format(CultureInfo.InvariantCulture, CommandStrings.RebuilderResourceNotFound, projectResource.Name) }; } var mainLogger = loggerService.GetLogger(projectResource); @@ -261,7 +261,7 @@ await resourceNotificationService.PublishUpdateAsync(projectResource, s => s wit { State = new ResourceStateSnapshot(KnownResourceStates.FailedToStart, KnownResourceStateStyles.Error) }).ConfigureAwait(false); - return new ExecuteCommandResult { Success = false, ErrorMessage = "Build timed out." }; + return new ExecuteCommandResult { Success = false, Message = "Build timed out." }; } if (exitCode == 0) @@ -316,7 +316,7 @@ await resourceNotificationService.PublishUpdateAsync(projectResource, name, s => } } - return CommandResults.Success(); + return new ExecuteCommandResult { Success = true, Message = string.Format(CultureInfo.InvariantCulture, CommandStrings.ResourceRebuilt, projectResource.Name) }; } else { @@ -325,7 +325,7 @@ await resourceNotificationService.PublishUpdateAsync(projectResource, s => s wit { State = new ResourceStateSnapshot(KnownResourceStates.FailedToStart, KnownResourceStateStyles.Error) }).ConfigureAwait(false); - return new ExecuteCommandResult { Success = false, ErrorMessage = $"Build failed with exit code {exitCode}." }; + return new ExecuteCommandResult { Success = false, Message = $"Build failed with exit code {exitCode}." }; } } catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) @@ -337,7 +337,7 @@ await resourceNotificationService.PublishUpdateAsync(projectResource, s => s wit { State = new ResourceStateSnapshot(KnownResourceStates.Finished, KnownResourceStateStyles.Info) }).ConfigureAwait(false); - return new ExecuteCommandResult { Success = false, ErrorMessage = "Rebuild was cancelled." }; + return new ExecuteCommandResult { Success = false, Message = "Rebuild was cancelled." }; } finally { diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs index 46513404b28..90e2f5bac0f 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs @@ -128,7 +128,12 @@ public enum CommandResultFormat /// /// JSON result. /// - Json + Json, + + /// + /// Markdown result. + /// + Markdown } /// @@ -142,17 +147,25 @@ public static class CommandResults public static ExecuteCommandResult Success() => new() { Success = true }; /// - /// Produces a success result with result data. + /// Produces a success result with a message and result data. /// + /// The message associated with the result. /// The result data. /// The format of the result data. Defaults to . - public static ExecuteCommandResult Success(string result, CommandResultFormat resultFormat = CommandResultFormat.Text) => new() { Success = true, Result = result, ResultFormat = resultFormat }; + public static ExecuteCommandResult Success(string message, string result, CommandResultFormat resultFormat = CommandResultFormat.Text) => new() { Success = true, Message = message, Data = new CommandResultData { Value = result, Format = resultFormat } }; + + /// + /// Produces a success result with a message and a value. + /// + /// The message associated with the result. + /// The value produced by the command. + public static ExecuteCommandResult Success(string message, CommandResultData value) => new() { Success = true, Message = message, Data = value }; /// /// Produces an unsuccessful result with an error message. /// /// An optional error message. - public static ExecuteCommandResult Failure(string? errorMessage = null) => new() { Success = false, ErrorMessage = errorMessage }; + public static ExecuteCommandResult Failure(string? errorMessage = null) => new() { Success = false, Message = errorMessage }; /// /// Produces an unsuccessful result with an error message and result data. @@ -160,7 +173,14 @@ public static class CommandResults /// The error message. /// The result data. /// The format of the result data. Defaults to . - public static ExecuteCommandResult Failure(string errorMessage, string result, CommandResultFormat resultFormat = CommandResultFormat.Text) => new() { Success = false, ErrorMessage = errorMessage, Result = result, ResultFormat = resultFormat }; + public static ExecuteCommandResult Failure(string errorMessage, string result, CommandResultFormat resultFormat = CommandResultFormat.Text) => new() { Success = false, Message = errorMessage, Data = new CommandResultData { Value = result, Format = resultFormat } }; + + /// + /// Produces an unsuccessful result with an error message and a value. + /// + /// The error message. + /// The value produced by the command. + public static ExecuteCommandResult Failure(string errorMessage, CommandResultData value) => new() { Success = false, Message = errorMessage, Data = value }; /// /// Produces a canceled result. @@ -193,17 +213,50 @@ public sealed class ExecuteCommandResult /// /// An optional error message that can be set when the command is unsuccessful. /// - public string? ErrorMessage { get; init; } + [Obsolete("Use Message instead.")] + public string? ErrorMessage + { + get => _message; + init => _message ??= value; + } + + /// + /// An optional message associated with the command result. + /// + public string? Message + { + get => _message; + init => _message = value; + } + + private string? _message; + + /// + /// An optional value produced by the command. + /// + public CommandResultData? Data { get; init; } +} + +/// +/// Represents a value produced by a command. +/// +[AspireDto] +public sealed class CommandResultData +{ + /// + /// The value data. + /// + public required string Value { get; init; } /// - /// An optional result value produced by the command. + /// The format of the data. /// - public string? Result { get; init; } + public CommandResultFormat Format { get; init; } /// - /// The format of the value. + /// When , the dashboard will immediately display the value in a dialog when the command completes. /// - public CommandResultFormat? ResultFormat { get; init; } + public bool DisplayImmediately { get; init; } } /// diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceCommandService.cs b/src/Aspire.Hosting/ApplicationModel/ResourceCommandService.cs index 95a4043e980..a1a6bd9f9ec 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceCommandService.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceCommandService.cs @@ -56,7 +56,7 @@ public async Task ExecuteCommandAsync(string resourceId, s { if (!_resourceNotificationService.TryGetCurrentState(resourceId, out var resourceEvent)) { - return new ExecuteCommandResult { Success = false, ErrorMessage = $"Resource '{resourceId}' not found." }; + return new ExecuteCommandResult { Success = false, Message = $"Resource '{resourceId}' not found." }; } return await ExecuteCommandCoreAsync(resourceEvent.ResourceId, resourceEvent.Resource, commandName, cancellationToken).ConfigureAwait(false); @@ -106,12 +106,11 @@ public async Task ExecuteCommandAsync(IResource resource, if (failures.Count == 0 && cancellations.Count == 0) { - var successWithResult = results.FirstOrDefault(r => r.Result is not null); + var successWithResult = results.FirstOrDefault(r => r.Data is not null); return new ExecuteCommandResult { Success = true, - Result = successWithResult?.Result, - ResultFormat = successWithResult?.ResultFormat + Data = successWithResult?.Data }; } else if (failures.Count == 0 && cancellations.Count > 0) @@ -123,12 +122,12 @@ public async Task ExecuteCommandAsync(IResource resource, { // There were actual failures (possibly with some cancellations) var errorMessage = $"{failures.Count} command executions failed."; - errorMessage += Environment.NewLine + string.Join(Environment.NewLine, failures.Select(f => $"Resource '{f.resourceId}' failed with error message: {f.result.ErrorMessage}")); + errorMessage += Environment.NewLine + string.Join(Environment.NewLine, failures.Select(f => $"Resource '{f.resourceId}' failed with error message: {f.result.Message}")); return new ExecuteCommandResult { Success = false, - ErrorMessage = errorMessage + Message = errorMessage }; } } @@ -178,7 +177,7 @@ internal async Task ExecuteCommandCoreAsync(string resourc } else { - logger.LogInformation("Failure executing command '{CommandName}'. Error message: {ErrorMessage}", commandName, result.ErrorMessage); + logger.LogInformation("Failure executing command '{CommandName}'. Error message: {ErrorMessage}", commandName, result.Message); return result; } } @@ -190,11 +189,11 @@ internal async Task ExecuteCommandCoreAsync(string resourc catch (Exception ex) { logger.LogError(ex, "Error executing command '{CommandName}'.", commandName); - return new ExecuteCommandResult { Success = false, ErrorMessage = "Unhandled exception thrown." }; + return new ExecuteCommandResult { Success = false, Message = "Unhandled exception thrown." }; } } logger.LogInformation("Command '{CommandName}' not available.", commandName); - return new ExecuteCommandResult { Success = false, ErrorMessage = $"Command '{commandName}' not available for resource '{resourceId}'." }; + return new ExecuteCommandResult { Success = false, Message = $"Command '{commandName}' not available for resource '{resourceId}'." }; } } diff --git a/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs b/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs index e833f6d8ec9..9b642300c6c 100644 --- a/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs +++ b/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs @@ -214,13 +214,29 @@ public async Task ExecuteResourceCommandAsync(Ex var resourceCommandService = serviceProvider.GetRequiredService(); var result = await resourceCommandService.ExecuteCommandAsync(request.ResourceName, request.CommandName, cancellationToken).ConfigureAwait(false); +#pragma warning disable CS0618 // Type or member is obsolete + var resolvedMessage = result.Message ?? result.ErrorMessage; +#pragma warning restore CS0618 // Type or member is obsolete + return new ExecuteResourceCommandResponse { Success = result.Success, Canceled = result.Canceled, - ErrorMessage = result.ErrorMessage, - Result = result.Result, - ResultFormat = result.ResultFormat?.ToString().ToLowerInvariant() +#pragma warning disable CS0618 // Type or member is obsolete + ErrorMessage = resolvedMessage, +#pragma warning restore CS0618 // Type or member is obsolete + Message = resolvedMessage, + Value = result.Data is { } v ? new ExecuteResourceCommandResult + { + Value = v.Value, + Format = v.Format switch + { + ApplicationModel.CommandResultFormat.Json => CommandResultFormat.Json, + ApplicationModel.CommandResultFormat.Markdown => CommandResultFormat.Markdown, + _ => CommandResultFormat.Text + }, + DisplayImmediately = v.DisplayImmediately + } : null }; } diff --git a/src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs b/src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs index 99eb58bf134..350906a0988 100644 --- a/src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs +++ b/src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs @@ -11,6 +11,7 @@ namespace Aspire.Hosting.Backchannel; using System.Diagnostics; using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using ModelContextProtocol.Protocol; @@ -284,17 +285,64 @@ internal sealed class ExecuteResourceCommandResponse /// /// Gets the error message if the command failed. /// + [Obsolete("Use Message instead.")] public string? ErrorMessage { get; init; } /// - /// Gets the result data produced by the command. + /// Gets the message associated with the command result. /// - public string? Result { get; init; } + public string? Message { get; init; } /// - /// Gets the format of the result data (e.g. "none", "text", "json"). + /// Gets the value produced by the command. /// - public string? ResultFormat { get; init; } + public ExecuteResourceCommandResult? Value { get; init; } +} + +/// +/// Value produced by a resource command. +/// +internal sealed class ExecuteResourceCommandResult +{ + /// + /// Gets the value data. + /// + public required string Value { get; init; } + + /// + /// Gets the format of the value data. + /// + public CommandResultFormat Format { get; init; } + + /// + /// Gets whether to immediately display the value in the dashboard. + /// + public bool DisplayImmediately { get; init; } +} + +/// +/// Specifies the format of a command result. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +internal enum CommandResultFormat +{ + /// + /// Plain text result. + /// + [JsonStringEnumMemberName("text")] + Text, + + /// + /// JSON result. + /// + [JsonStringEnumMemberName("json")] + Json, + + /// + /// Markdown result. + /// + [JsonStringEnumMemberName("markdown")] + Markdown } #endregion diff --git a/src/Aspire.Hosting/Dashboard/DashboardService.cs b/src/Aspire.Hosting/Dashboard/DashboardService.cs index b671c7a1890..54f5b1591e2 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardService.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardService.cs @@ -360,7 +360,7 @@ async Task WatchResourceConsoleLogsInternal(bool suppressFollow, CancellationTok public override async Task ExecuteResourceCommand(ResourceCommandRequest request, ServerCallContext context) { - var (result, errorMessage, commandResult, resultFormat) = await serviceData.ExecuteCommandAsync(request.ResourceName, request.CommandName, context.CancellationToken).ConfigureAwait(false); + var (result, message, value) = await serviceData.ExecuteCommandAsync(request.ResourceName, request.CommandName, context.CancellationToken).ConfigureAwait(false); var responseKind = result switch { ExecuteCommandResultType.Success => ResourceCommandResponseKind.Succeeded, @@ -372,18 +372,29 @@ public override async Task ExecuteResourceCommand(Resou var response = new ResourceCommandResponse { Kind = responseKind, - ErrorMessage = errorMessage ?? string.Empty, - ResultFormat = resultFormat switch + Message = message ?? string.Empty, + }; + +#pragma warning disable CS0612 // Type or member is obsolete + response.ErrorMessage = message ?? string.Empty; +#pragma warning restore CS0612 // Type or member is obsolete + + if (value is not null) + { + static Aspire.DashboardService.Proto.V1.CommandResultFormat MapFormat(ApplicationModel.CommandResultFormat format) => format switch { ApplicationModel.CommandResultFormat.Text => Aspire.DashboardService.Proto.V1.CommandResultFormat.Text, ApplicationModel.CommandResultFormat.Json => Aspire.DashboardService.Proto.V1.CommandResultFormat.Json, + ApplicationModel.CommandResultFormat.Markdown => Aspire.DashboardService.Proto.V1.CommandResultFormat.Markdown, _ => Aspire.DashboardService.Proto.V1.CommandResultFormat.None - } - }; + }; - if (commandResult is not null) - { - response.Result = commandResult; + response.Result = new ResourceCommandResult + { + Value = value.Value, + Format = MapFormat(value.Format), + DisplayImmediately = value.DisplayImmediately + }; } return response; diff --git a/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs b/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs index dd887bb3bc5..b1f3a6802fd 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs @@ -94,21 +94,21 @@ public void Dispose() _cts.Dispose(); } - internal async Task<(ExecuteCommandResultType result, string? errorMessage, string? commandResult, ApplicationModel.CommandResultFormat? resultFormat)> ExecuteCommandAsync(string resourceId, string type, CancellationToken cancellationToken) + internal async Task<(ExecuteCommandResultType result, string? message, ApplicationModel.CommandResultData? value)> ExecuteCommandAsync(string resourceId, string type, CancellationToken cancellationToken) { try { var result = await _resourceCommandService.ExecuteCommandAsync(resourceId, type, cancellationToken).ConfigureAwait(false); if (result.Canceled) { - return (ExecuteCommandResultType.Canceled, result.ErrorMessage, null, null); + return (ExecuteCommandResultType.Canceled, result.Message, null); } - return (result.Success ? ExecuteCommandResultType.Success : ExecuteCommandResultType.Failure, result.ErrorMessage, result.Result, result.ResultFormat); + return (result.Success ? ExecuteCommandResultType.Success : ExecuteCommandResultType.Failure, result.Message, result.Data); } catch { // Note: Exception is already logged in the command executor. - return (ExecuteCommandResultType.Failure, "Unhandled exception thrown while executing command.", null, null); + return (ExecuteCommandResultType.Failure, "Unhandled exception thrown while executing command.", null); } } diff --git a/src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto b/src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto index 5eb948c2fd2..23ceff6e0b4 100644 --- a/src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto +++ b/src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto @@ -91,15 +91,22 @@ enum ResourceCommandResponseKind { message ResourceCommandResponse { ResourceCommandResponseKind kind = 1; - optional string error_message = 2; - optional string result = 3; - CommandResultFormat result_format = 4; + optional string error_message = 2 [deprecated = true]; + optional string message = 3; + optional ResourceCommandResult result = 4; +} + +message ResourceCommandResult { + string value = 1; + CommandResultFormat format = 2; + bool display_immediately = 3; } enum CommandResultFormat { COMMAND_RESULT_FORMAT_NONE = 0; COMMAND_RESULT_FORMAT_TEXT = 1; COMMAND_RESULT_FORMAT_JSON = 2; + COMMAND_RESULT_FORMAT_MARKDOWN = 3; } //////////////////////////////////////////// diff --git a/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs b/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs index 07585658bcd..02eeda22989 100644 --- a/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs +++ b/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs @@ -200,7 +200,7 @@ private void AddSetParameterCommand(ParameterResource parameterResource) executeCommand: async context => { await SetParameterAsync(parameterResource, context.CancellationToken).ConfigureAwait(false); - return CommandResults.Success(); + return new ExecuteCommandResult { Success = true, Message = string.Format(CultureInfo.InvariantCulture, CommandStrings.ResourceSetParameter, parameterResource.Name) }; }, updateState: _ => ResourceCommandState.Enabled, displayDescription: CommandStrings.SetParameterDescription, @@ -216,7 +216,7 @@ private void AddSetParameterCommand(ParameterResource parameterResource) executeCommand: async context => { await DeleteParameterAsync(parameterResource, context.CancellationToken).ConfigureAwait(false); - return CommandResults.Success(); + return new ExecuteCommandResult { Success = true, Message = string.Format(CultureInfo.InvariantCulture, CommandStrings.ResourceDeletedParameter, parameterResource.Name) }; }, updateState: _ => HasParameterValue(parameterResource) ? ResourceCommandState.Enabled : ResourceCommandState.Hidden, displayDescription: CommandStrings.DeleteParameterDescription, diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index d6d836910e2..90f41cdc6ca 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -2640,9 +2640,8 @@ public static IResourceBuilder WithHttpCommand( { if (!endpoint.IsAllocated) { - return new ExecuteCommandResult { Success = false, ErrorMessage = "Endpoints are not yet allocated." }; + return new ExecuteCommandResult { Success = false, Message = "Endpoints are not yet allocated." }; } - var uri = new UriBuilder(endpoint.Url) { Path = path }.Uri; var httpClient = context.ServiceProvider.GetRequiredService().CreateClient(commandOptions.HttpClientName ?? Options.DefaultName); var request = new HttpRequestMessage(commandOptions.Method, uri); diff --git a/src/Aspire.Hosting/Resources/CommandStrings.Designer.cs b/src/Aspire.Hosting/Resources/CommandStrings.Designer.cs index 88d74edfc8a..6135bb20f2a 100644 --- a/src/Aspire.Hosting/Resources/CommandStrings.Designer.cs +++ b/src/Aspire.Hosting/Resources/CommandStrings.Designer.cs @@ -132,6 +132,60 @@ internal static string RebuilderResourceNotFound { } } + /// + /// Looks up a localized string similar to Successfully deleted parameter '{0}'.. + /// + internal static string ResourceDeletedParameter { + get { + return ResourceManager.GetString("ResourceDeletedParameter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Successfully rebuilt '{0}'.. + /// + internal static string ResourceRebuilt { + get { + return ResourceManager.GetString("ResourceRebuilt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Successfully restarted '{0}'.. + /// + internal static string ResourceRestarted { + get { + return ResourceManager.GetString("ResourceRestarted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Successfully set parameter '{0}'.. + /// + internal static string ResourceSetParameter { + get { + return ResourceManager.GetString("ResourceSetParameter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Successfully started '{0}'.. + /// + internal static string ResourceStarted { + get { + return ResourceManager.GetString("ResourceStarted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Successfully stopped '{0}'.. + /// + internal static string ResourceStopped { + get { + return ResourceManager.GetString("ResourceStopped", resourceCulture); + } + } + /// /// Looks up a localized string similar to Set parameter value. /// diff --git a/src/Aspire.Hosting/Resources/CommandStrings.resx b/src/Aspire.Hosting/Resources/CommandStrings.resx index d8d65915ec1..c955543bf60 100644 --- a/src/Aspire.Hosting/Resources/CommandStrings.resx +++ b/src/Aspire.Hosting/Resources/CommandStrings.resx @@ -159,4 +159,28 @@ Rebuilder resource for '{0}' not found. + + Successfully deleted parameter '{0}'. + {0} is the resource name. + + + Successfully rebuilt '{0}'. + {0} is the resource name. + + + Successfully restarted '{0}'. + {0} is the resource name. + + + Successfully set parameter '{0}'. + {0} is the resource name. + + + Successfully started '{0}'. + {0} is the resource name. + + + Successfully stopped '{0}'. + {0} is the resource name. + diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.cs.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.cs.xlf index d4e380fc1c3..ebde0302553 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.cs.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.cs.xlf @@ -22,6 +22,36 @@ Rebuild + + Successfully deleted parameter '{0}'. + Successfully deleted parameter '{0}'. + {0} is the resource name. + + + Successfully rebuilt '{0}'. + Successfully rebuilt '{0}'. + {0} is the resource name. + + + Successfully restarted '{0}'. + Successfully restarted '{0}'. + {0} is the resource name. + + + Successfully set parameter '{0}'. + Successfully set parameter '{0}'. + {0} is the resource name. + + + Successfully started '{0}'. + Successfully started '{0}'. + {0} is the resource name. + + + Successfully stopped '{0}'. + Successfully stopped '{0}'. + {0} is the resource name. + Restart resource Restartovat prostředek @@ -74,4 +104,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.de.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.de.xlf index 949950b0c3b..adbf6bbe364 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.de.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.de.xlf @@ -22,6 +22,36 @@ Rebuild + + Successfully deleted parameter '{0}'. + Successfully deleted parameter '{0}'. + {0} is the resource name. + + + Successfully rebuilt '{0}'. + Successfully rebuilt '{0}'. + {0} is the resource name. + + + Successfully restarted '{0}'. + Successfully restarted '{0}'. + {0} is the resource name. + + + Successfully set parameter '{0}'. + Successfully set parameter '{0}'. + {0} is the resource name. + + + Successfully started '{0}'. + Successfully started '{0}'. + {0} is the resource name. + + + Successfully stopped '{0}'. + Successfully stopped '{0}'. + {0} is the resource name. + Restart resource Ressource neu starten @@ -74,4 +104,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.es.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.es.xlf index 90bb9e812a2..d2f67ba89e0 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.es.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.es.xlf @@ -22,6 +22,36 @@ Rebuild + + Successfully deleted parameter '{0}'. + Successfully deleted parameter '{0}'. + {0} is the resource name. + + + Successfully rebuilt '{0}'. + Successfully rebuilt '{0}'. + {0} is the resource name. + + + Successfully restarted '{0}'. + Successfully restarted '{0}'. + {0} is the resource name. + + + Successfully set parameter '{0}'. + Successfully set parameter '{0}'. + {0} is the resource name. + + + Successfully started '{0}'. + Successfully started '{0}'. + {0} is the resource name. + + + Successfully stopped '{0}'. + Successfully stopped '{0}'. + {0} is the resource name. + Restart resource Reiniciar recurso @@ -74,4 +104,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.fr.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.fr.xlf index d398468211a..f69fe25f71a 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.fr.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.fr.xlf @@ -22,6 +22,36 @@ Rebuild + + Successfully deleted parameter '{0}'. + Successfully deleted parameter '{0}'. + {0} is the resource name. + + + Successfully rebuilt '{0}'. + Successfully rebuilt '{0}'. + {0} is the resource name. + + + Successfully restarted '{0}'. + Successfully restarted '{0}'. + {0} is the resource name. + + + Successfully set parameter '{0}'. + Successfully set parameter '{0}'. + {0} is the resource name. + + + Successfully started '{0}'. + Successfully started '{0}'. + {0} is the resource name. + + + Successfully stopped '{0}'. + Successfully stopped '{0}'. + {0} is the resource name. + Restart resource Redémarrer la ressource @@ -74,4 +104,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.it.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.it.xlf index 09a36f5672c..783c0e16512 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.it.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.it.xlf @@ -22,6 +22,36 @@ Rebuild + + Successfully deleted parameter '{0}'. + Successfully deleted parameter '{0}'. + {0} is the resource name. + + + Successfully rebuilt '{0}'. + Successfully rebuilt '{0}'. + {0} is the resource name. + + + Successfully restarted '{0}'. + Successfully restarted '{0}'. + {0} is the resource name. + + + Successfully set parameter '{0}'. + Successfully set parameter '{0}'. + {0} is the resource name. + + + Successfully started '{0}'. + Successfully started '{0}'. + {0} is the resource name. + + + Successfully stopped '{0}'. + Successfully stopped '{0}'. + {0} is the resource name. + Restart resource Riavvia risorsa @@ -74,4 +104,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.ja.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.ja.xlf index b97008f9ba1..4a3f1fa42d8 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.ja.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.ja.xlf @@ -22,6 +22,36 @@ Rebuild + + Successfully deleted parameter '{0}'. + Successfully deleted parameter '{0}'. + {0} is the resource name. + + + Successfully rebuilt '{0}'. + Successfully rebuilt '{0}'. + {0} is the resource name. + + + Successfully restarted '{0}'. + Successfully restarted '{0}'. + {0} is the resource name. + + + Successfully set parameter '{0}'. + Successfully set parameter '{0}'. + {0} is the resource name. + + + Successfully started '{0}'. + Successfully started '{0}'. + {0} is the resource name. + + + Successfully stopped '{0}'. + Successfully stopped '{0}'. + {0} is the resource name. + Restart resource リソースの再起動 @@ -74,4 +104,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.ko.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.ko.xlf index 459de777333..16ac639329f 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.ko.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.ko.xlf @@ -22,6 +22,36 @@ Rebuild + + Successfully deleted parameter '{0}'. + Successfully deleted parameter '{0}'. + {0} is the resource name. + + + Successfully rebuilt '{0}'. + Successfully rebuilt '{0}'. + {0} is the resource name. + + + Successfully restarted '{0}'. + Successfully restarted '{0}'. + {0} is the resource name. + + + Successfully set parameter '{0}'. + Successfully set parameter '{0}'. + {0} is the resource name. + + + Successfully started '{0}'. + Successfully started '{0}'. + {0} is the resource name. + + + Successfully stopped '{0}'. + Successfully stopped '{0}'. + {0} is the resource name. + Restart resource 리소스 다시 시작 @@ -74,4 +104,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.pl.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.pl.xlf index f79143807e8..f959a404a13 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.pl.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.pl.xlf @@ -22,6 +22,36 @@ Rebuild + + Successfully deleted parameter '{0}'. + Successfully deleted parameter '{0}'. + {0} is the resource name. + + + Successfully rebuilt '{0}'. + Successfully rebuilt '{0}'. + {0} is the resource name. + + + Successfully restarted '{0}'. + Successfully restarted '{0}'. + {0} is the resource name. + + + Successfully set parameter '{0}'. + Successfully set parameter '{0}'. + {0} is the resource name. + + + Successfully started '{0}'. + Successfully started '{0}'. + {0} is the resource name. + + + Successfully stopped '{0}'. + Successfully stopped '{0}'. + {0} is the resource name. + Restart resource Uruchom ponownie zasób @@ -74,4 +104,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.pt-BR.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.pt-BR.xlf index f9a249651f6..c89ebb00271 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.pt-BR.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.pt-BR.xlf @@ -22,6 +22,36 @@ Rebuild + + Successfully deleted parameter '{0}'. + Successfully deleted parameter '{0}'. + {0} is the resource name. + + + Successfully rebuilt '{0}'. + Successfully rebuilt '{0}'. + {0} is the resource name. + + + Successfully restarted '{0}'. + Successfully restarted '{0}'. + {0} is the resource name. + + + Successfully set parameter '{0}'. + Successfully set parameter '{0}'. + {0} is the resource name. + + + Successfully started '{0}'. + Successfully started '{0}'. + {0} is the resource name. + + + Successfully stopped '{0}'. + Successfully stopped '{0}'. + {0} is the resource name. + Restart resource Reiniciar recurso @@ -74,4 +104,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.ru.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.ru.xlf index 98e0deb2bc9..e590c123ad5 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.ru.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.ru.xlf @@ -22,6 +22,36 @@ Rebuild + + Successfully deleted parameter '{0}'. + Successfully deleted parameter '{0}'. + {0} is the resource name. + + + Successfully rebuilt '{0}'. + Successfully rebuilt '{0}'. + {0} is the resource name. + + + Successfully restarted '{0}'. + Successfully restarted '{0}'. + {0} is the resource name. + + + Successfully set parameter '{0}'. + Successfully set parameter '{0}'. + {0} is the resource name. + + + Successfully started '{0}'. + Successfully started '{0}'. + {0} is the resource name. + + + Successfully stopped '{0}'. + Successfully stopped '{0}'. + {0} is the resource name. + Restart resource Перезапустить ресурс @@ -74,4 +104,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.tr.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.tr.xlf index 8e222250001..fb14686e425 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.tr.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.tr.xlf @@ -22,6 +22,36 @@ Rebuild + + Successfully deleted parameter '{0}'. + Successfully deleted parameter '{0}'. + {0} is the resource name. + + + Successfully rebuilt '{0}'. + Successfully rebuilt '{0}'. + {0} is the resource name. + + + Successfully restarted '{0}'. + Successfully restarted '{0}'. + {0} is the resource name. + + + Successfully set parameter '{0}'. + Successfully set parameter '{0}'. + {0} is the resource name. + + + Successfully started '{0}'. + Successfully started '{0}'. + {0} is the resource name. + + + Successfully stopped '{0}'. + Successfully stopped '{0}'. + {0} is the resource name. + Restart resource Kaynağı yeniden başlat @@ -74,4 +104,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hans.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hans.xlf index 667a0defe83..430e2a03c4d 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hans.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hans.xlf @@ -22,6 +22,36 @@ Rebuild + + Successfully deleted parameter '{0}'. + Successfully deleted parameter '{0}'. + {0} is the resource name. + + + Successfully rebuilt '{0}'. + Successfully rebuilt '{0}'. + {0} is the resource name. + + + Successfully restarted '{0}'. + Successfully restarted '{0}'. + {0} is the resource name. + + + Successfully set parameter '{0}'. + Successfully set parameter '{0}'. + {0} is the resource name. + + + Successfully started '{0}'. + Successfully started '{0}'. + {0} is the resource name. + + + Successfully stopped '{0}'. + Successfully stopped '{0}'. + {0} is the resource name. + Restart resource 重启资源 @@ -74,4 +104,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hant.xlf b/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hant.xlf index 2f18001c65d..5b5ef0297a8 100644 --- a/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hant.xlf +++ b/src/Aspire.Hosting/Resources/xlf/CommandStrings.zh-Hant.xlf @@ -22,6 +22,36 @@ Rebuild + + Successfully deleted parameter '{0}'. + Successfully deleted parameter '{0}'. + {0} is the resource name. + + + Successfully rebuilt '{0}'. + Successfully rebuilt '{0}'. + {0} is the resource name. + + + Successfully restarted '{0}'. + Successfully restarted '{0}'. + {0} is the resource name. + + + Successfully set parameter '{0}'. + Successfully set parameter '{0}'. + {0} is the resource name. + + + Successfully started '{0}'. + Successfully started '{0}'. + {0} is the resource name. + + + Successfully stopped '{0}'. + Successfully stopped '{0}'. + {0} is the resource name. + Restart resource 重新啟動資源 @@ -74,4 +104,4 @@ - + \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs index cccc8c50408..78fdd454e5c 100644 --- a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs @@ -1776,7 +1776,7 @@ public void DisplaySubtleMessage(string message, bool escapeMarkup = true) { } public void DisplayEmptyLine() { } public void DisplayPlainText(string text) { } public void DisplayRawText(string text, ConsoleOutput? consoleOverride = null) { } - public void DisplayMarkdown(string markdown) { } + public void DisplayMarkdown(string markdown, ConsoleOutput? consoleOverride = null) { } public void DisplayMarkupLine(string markup) { } public void WriteConsoleLog(string message, int? lineNumber = null, string? type = null, bool isErrorMessage = false) { } public void DisplayVersionUpdateNotification(string newerVersion, string? updateCommand = null) { } diff --git a/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs b/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs index 032e55cece1..9be6f55cfdf 100644 --- a/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs @@ -958,7 +958,7 @@ public void DisplayCancellationMessage() { } public void DisplayEmptyLine() { } public void DisplayPlainText(string text) { } public void DisplayRawText(string text, ConsoleOutput? consoleOverride = null) { } - public void DisplayMarkdown(string markdown) { } + public void DisplayMarkdown(string markdown, ConsoleOutput? consoleOverride = null) { } public void DisplayMarkupLine(string markup) { } public void DisplayVersionUpdateNotification(string newerVersion, string? updateCommand = null) { } diff --git a/tests/Aspire.Cli.Tests/Commands/ResourceCommandHelperTests.cs b/tests/Aspire.Cli.Tests/Commands/ResourceCommandHelperTests.cs index 9970af18a47..ed852d4f43e 100644 --- a/tests/Aspire.Cli.Tests/Commands/ResourceCommandHelperTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/ResourceCommandHelperTests.cs @@ -20,8 +20,11 @@ public async Task ExecuteGenericCommandAsync_WithResult_OutputsRawText() ExecuteResourceCommandResult = new ExecuteResourceCommandResponse { Success = true, - Result = "{\"items\": [\"a\", \"b\"]}", - ResultFormat = "json" + Value = new ExecuteResourceCommandResult + { + Value = "{\"items\": [\"a\", \"b\"]}", + Format = CommandResultFormat.Json + } } }; @@ -79,9 +82,12 @@ public async Task ExecuteGenericCommandAsync_ErrorWithResult_OutputsRawText() ExecuteResourceCommandResult = new ExecuteResourceCommandResponse { Success = false, - ErrorMessage = "Validation failed", - Result = "{\"errors\": [\"invalid host\"]}", - ResultFormat = "json" + Message = "Validation failed", + Value = new ExecuteResourceCommandResult + { + Value = "{\"errors\": [\"invalid host\"]}", + Format = CommandResultFormat.Json + } } }; @@ -112,8 +118,11 @@ public async Task ExecuteGenericCommandAsync_RoutesStatusToStderr_ResultToStdout ExecuteResourceCommandResult = new ExecuteResourceCommandResponse { Success = true, - Result = "some output", - ResultFormat = "text" + Value = new ExecuteResourceCommandResult + { + Value = "some output", + Format = CommandResultFormat.Text + } } }; diff --git a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs index 8872ea4d474..6f622c9edc4 100644 --- a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs @@ -1040,7 +1040,7 @@ public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, stri public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false) => _innerService.DisplayMessage(emoji, message, allowMarkup); public void DisplayPlainText(string text) => _innerService.DisplayPlainText(text); public void DisplayRawText(string text, ConsoleOutput? consoleOverride = null) => _innerService.DisplayRawText(text, consoleOverride); - public void DisplayMarkdown(string markdown) => _innerService.DisplayMarkdown(markdown); + public void DisplayMarkdown(string markdown, ConsoleOutput? consoleOverride = null) => _innerService.DisplayMarkdown(markdown, consoleOverride); public void DisplayMarkupLine(string markup) => _innerService.DisplayMarkupLine(markup); public void DisplaySuccess(string message, bool allowMarkup = false) => _innerService.DisplaySuccess(message, allowMarkup); public void DisplaySubtleMessage(string message, bool allowMarkup = false) => _innerService.DisplaySubtleMessage(message, allowMarkup); diff --git a/tests/Aspire.Cli.Tests/Mcp/ExecuteResourceCommandToolTests.cs b/tests/Aspire.Cli.Tests/Mcp/ExecuteResourceCommandToolTests.cs index 8926e7a97d4..c26f5ee4e92 100644 --- a/tests/Aspire.Cli.Tests/Mcp/ExecuteResourceCommandToolTests.cs +++ b/tests/Aspire.Cli.Tests/Mcp/ExecuteResourceCommandToolTests.cs @@ -69,7 +69,7 @@ public async Task ExecuteResourceCommandTool_ReturnsError_WhenCommandFails() ExecuteResourceCommandResult = new ExecuteResourceCommandResponse { Success = false, - ErrorMessage = "Resource not found" + Message = "Resource not found" } }; monitor.AddConnection("hash1", "socket.hash1", connection); @@ -160,8 +160,11 @@ public async Task ExecuteResourceCommandTool_ReturnsResult_WhenCommandReturnsRes ExecuteResourceCommandResult = new ExecuteResourceCommandResponse { Success = true, - Result = "{\"token\": \"abc123\"}", - ResultFormat = "json" + Value = new ExecuteResourceCommandResult + { + Value = "{\"token\": \"abc123\"}", + Format = CommandResultFormat.Json + } } }; monitor.AddConnection("hash1", "socket.hash1", connection); diff --git a/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs b/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs index 52670fb8571..851e04daec2 100644 --- a/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs @@ -173,7 +173,7 @@ public Task LaunchAppHostAsync(string projectFile, List arguments, List< public void DisplayEmptyLine() => throw new NotImplementedException(); public void DisplayPlainText(string text) => throw new NotImplementedException(); public void DisplayRawText(string text, ConsoleOutput? consoleOverride = null) => throw new NotImplementedException(); - public void DisplayMarkdown(string markdown) => throw new NotImplementedException(); + public void DisplayMarkdown(string markdown, ConsoleOutput? consoleOverride = null) => throw new NotImplementedException(); public void DisplayMarkupLine(string markup) => throw new NotImplementedException(); public void DisplayVersionUpdateNotification(string newerVersion, string? updateCommand = null) => throw new NotImplementedException(); public void DisplayRenderable(Spectre.Console.Rendering.IRenderable renderable) => throw new NotImplementedException(); diff --git a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs index 45e050dafb2..62c91aa0b90 100644 --- a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs +++ b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs @@ -486,7 +486,7 @@ public void DisplayCancellationMessage() { } public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, string appHostHostingVersion) => 0; public void DisplayPlainText(string text) { } public void DisplayRawText(string text, ConsoleOutput? consoleOverride = null) { } - public void DisplayMarkdown(string markdown) { } + public void DisplayMarkdown(string markdown, ConsoleOutput? consoleOverride = null) { } public void DisplayMarkupLine(string markup) { } public void DisplaySubtleMessage(string message, bool allowMarkup = false) { } public void DisplayEmptyLine() { } diff --git a/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs b/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs index a27e9ae7c6c..0fd0a1221dd 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs @@ -143,7 +143,7 @@ public void DisplayRawText(string text, ConsoleOutput? consoleOverride = null) { } - public void DisplayMarkdown(string markdown) + public void DisplayMarkdown(string markdown, ConsoleOutput? consoleOverride = null) { } diff --git a/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs b/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs index 7b3849549f2..4071aa826b4 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs @@ -217,7 +217,7 @@ public void DisplayRawText(string text, ConsoleOutput? consoleOverride = null) DisplayRawTextCallback?.Invoke(text); } - public void DisplayMarkdown(string markdown) + public void DisplayMarkdown(string markdown, ConsoleOutput? consoleOverride = null) { } diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/TextVisualizerDialogTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/TextVisualizerDialogTests.cs index 87859dd3312..f8d480d8e6d 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Controls/TextVisualizerDialogTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/TextVisualizerDialogTests.cs @@ -48,7 +48,7 @@ public async Task Render_TextVisualizerDialog_WithValidJson_FormatsJsonAsync() Assert.Equal(expectedJson, instance.TextVisualizerViewModel.FormattedText); Assert.Equal(DashboardUIHelpers.JsonFormat, instance.TextVisualizerViewModel.FormatKind); - Assert.Equal([DashboardUIHelpers.JsonFormat, DashboardUIHelpers.PlaintextFormat], instance.EnabledOptions.ToImmutableSortedSet()); + Assert.Equal([DashboardUIHelpers.JsonFormat, DashboardUIHelpers.MarkdownFormat, DashboardUIHelpers.PlaintextFormat], instance.EnabledOptions.ToImmutableSortedSet()); } [Fact] @@ -70,7 +70,7 @@ public async Task Render_TextVisualizerDialog_WithValidXml_FormatsXml_CanChangeF Assert.Equal(DashboardUIHelpers.XmlFormat, instance.TextVisualizerViewModel.FormatKind); Assert.Equal(expectedXml, instance.TextVisualizerViewModel.FormattedText); - Assert.Equal([DashboardUIHelpers.PlaintextFormat, DashboardUIHelpers.XmlFormat], instance.EnabledOptions.ToImmutableSortedSet()); + Assert.Equal([DashboardUIHelpers.MarkdownFormat, DashboardUIHelpers.PlaintextFormat, DashboardUIHelpers.XmlFormat], instance.EnabledOptions.ToImmutableSortedSet()); // changing format works instance.ChangeFormat(DashboardUIHelpers.PlaintextFormat, rawXml); @@ -97,7 +97,7 @@ public async Task Render_TextVisualizerDialog_WithValidXml_FormatsXmlWithDoctype Assert.Equal(DashboardUIHelpers.XmlFormat, instance.TextVisualizerViewModel.FormatKind); Assert.Equal(expectedXml, instance.TextVisualizerViewModel.FormattedText); - Assert.Equal([DashboardUIHelpers.PlaintextFormat, DashboardUIHelpers.XmlFormat], instance.EnabledOptions.ToImmutableSortedSet()); + Assert.Equal([DashboardUIHelpers.MarkdownFormat, DashboardUIHelpers.PlaintextFormat, DashboardUIHelpers.XmlFormat], instance.EnabledOptions.ToImmutableSortedSet()); } [Fact] @@ -113,7 +113,7 @@ public async Task Render_TextVisualizerDialog_WithInvalidJson_FormatsPlaintextAs Assert.Equal(DashboardUIHelpers.PlaintextFormat, instance.TextVisualizerViewModel.FormatKind); Assert.Equal(rawText, instance.TextVisualizerViewModel.FormattedText); - Assert.Equal([DashboardUIHelpers.PlaintextFormat], instance.EnabledOptions.ToImmutableSortedSet()); + Assert.Equal([DashboardUIHelpers.MarkdownFormat, DashboardUIHelpers.PlaintextFormat], instance.EnabledOptions.ToImmutableSortedSet()); } [Fact] diff --git a/tests/Aspire.Dashboard.Components.Tests/Shared/FluentUISetupHelpers.cs b/tests/Aspire.Dashboard.Components.Tests/Shared/FluentUISetupHelpers.cs index 54944181923..5a8bee75f9f 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Shared/FluentUISetupHelpers.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Shared/FluentUISetupHelpers.cs @@ -148,6 +148,8 @@ public static void AddCommonDashboardServices( context.Services.AddSingleton(themeManager ?? new ThemeManager(new TestThemeResolver())); context.Services.AddSingleton(); context.Services.AddSingleton(); + context.Services.AddSingleton(TimeProvider.System); + context.Services.AddSingleton(); context.Services.AddScoped(); context.Services.AddScoped(); context.Services.AddScoped(); diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index 7890993632f..e2c4d4873aa 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -133,6 +133,7 @@ type CommandResultFormat string const ( CommandResultFormatText CommandResultFormat = "Text" CommandResultFormatJson CommandResultFormat = "Json" + CommandResultFormatMarkdown CommandResultFormat = "Markdown" ) // UrlDisplayLocation represents UrlDisplayLocation. @@ -243,8 +244,8 @@ type ExecuteCommandResult struct { Success bool `json:"Success,omitempty"` Canceled bool `json:"Canceled,omitempty"` ErrorMessage string `json:"ErrorMessage,omitempty"` - Result string `json:"Result,omitempty"` - ResultFormat CommandResultFormat `json:"ResultFormat,omitempty"` + Message string `json:"Message,omitempty"` + Data *CommandResultData `json:"Data,omitempty"` } // ToMap converts the DTO to a map for JSON serialization. @@ -253,8 +254,24 @@ func (d *ExecuteCommandResult) ToMap() map[string]any { "Success": SerializeValue(d.Success), "Canceled": SerializeValue(d.Canceled), "ErrorMessage": SerializeValue(d.ErrorMessage), - "Result": SerializeValue(d.Result), - "ResultFormat": SerializeValue(d.ResultFormat), + "Message": SerializeValue(d.Message), + "Data": SerializeValue(d.Data), + } +} + +// CommandResultData represents CommandResultData. +type CommandResultData struct { + Value string `json:"Value,omitempty"` + Format CommandResultFormat `json:"Format,omitempty"` + DisplayImmediately bool `json:"DisplayImmediately,omitempty"` +} + +// ToMap converts the DTO to a map for JSON serialization. +func (d *CommandResultData) ToMap() map[string]any { + return map[string]any{ + "Value": SerializeValue(d.Value), + "Format": SerializeValue(d.Format), + "DisplayImmediately": SerializeValue(d.DisplayImmediately), } } diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index 9c19bdd9406..6a100c68e05 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -1,4 +1,4 @@ -// ===== AddDockerfileOptions.java ===== +// ===== AddDockerfileOptions.java ===== // AddDockerfileOptions.java - GENERATED CODE - DO NOT EDIT package aspire; @@ -2834,6 +2834,36 @@ public Map toMap() { } } +// ===== CommandResultData.java ===== +// CommandResultData.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** CommandResultData DTO. */ +public class CommandResultData { + private String value; + private CommandResultFormat format; + private boolean displayImmediately; + + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } + public CommandResultFormat getFormat() { return format; } + public void setFormat(CommandResultFormat value) { this.format = value; } + public boolean getDisplayImmediately() { return displayImmediately; } + public void setDisplayImmediately(boolean value) { this.displayImmediately = value; } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("Value", AspireClient.serializeValue(value)); + map.put("Format", AspireClient.serializeValue(format)); + map.put("DisplayImmediately", AspireClient.serializeValue(displayImmediately)); + return map; + } +} + // ===== CommandResultFormat.java ===== // CommandResultFormat.java - GENERATED CODE - DO NOT EDIT @@ -2845,7 +2875,8 @@ public Map toMap() { /** CommandResultFormat enum. */ public enum CommandResultFormat implements WireValueEnum { TEXT("Text"), - JSON("Json"); + JSON("Json"), + MARKDOWN("Markdown"); private final String value; @@ -9458,8 +9489,8 @@ public class ExecuteCommandResult { private boolean success; private boolean canceled; private String errorMessage; - private String result; - private CommandResultFormat resultFormat; + private String message; + private CommandResultData data; public boolean getSuccess() { return success; } public void setSuccess(boolean value) { this.success = value; } @@ -9467,18 +9498,18 @@ public class ExecuteCommandResult { public void setCanceled(boolean value) { this.canceled = value; } public String getErrorMessage() { return errorMessage; } public void setErrorMessage(String value) { this.errorMessage = value; } - public String getResult() { return result; } - public void setResult(String value) { this.result = value; } - public CommandResultFormat getResultFormat() { return resultFormat; } - public void setResultFormat(CommandResultFormat value) { this.resultFormat = value; } + public String getMessage() { return message; } + public void setMessage(String value) { this.message = value; } + public CommandResultData getData() { return data; } + public void setData(CommandResultData value) { this.data = value; } public Map toMap() { Map map = new HashMap<>(); map.put("Success", AspireClient.serializeValue(success)); map.put("Canceled", AspireClient.serializeValue(canceled)); map.put("ErrorMessage", AspireClient.serializeValue(errorMessage)); - map.put("Result", AspireClient.serializeValue(result)); - map.put("ResultFormat", AspireClient.serializeValue(resultFormat)); + map.put("Message", AspireClient.serializeValue(message)); + map.put("Data", AspireClient.serializeValue(data)); return map; } } @@ -20708,6 +20739,7 @@ public WithVolumeOptions isReadOnly(Boolean value) { .modules/CertificateTrustScope.java .modules/CommandLineArgsCallbackContext.java .modules/CommandOptions.java +.modules/CommandResultData.java .modules/CommandResultFormat.java .modules/CompleteStepMarkdownOptions.java .modules/CompleteStepOptions.java diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index fcc8f4e2a7a..6b7c8614357 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1496,7 +1496,7 @@ def _validate_dict_types(args: typing.Any, arg_types: typing.Any) -> bool: CertificateTrustScope = typing.Literal["None", "Append", "Override", "System"] -CommandResultFormat = typing.Literal["Text", "Json"] +CommandResultFormat = typing.Literal["Text", "Json", "Markdown"] ContainerLifetime = typing.Literal["Session", "Persistent"] @@ -1671,6 +1671,11 @@ class CommandOptions(typing.TypedDict, total=False): IsHighlighted: bool UpdateState: typing.Any +class CommandResultData(typing.TypedDict, total=False): + Value: str + Format: CommandResultFormat + DisplayImmediately: bool + class CreateBuilderOptions(typing.TypedDict, total=False): Args: typing.Iterable[str] ProjectDirectory: str @@ -1685,8 +1690,8 @@ class ExecuteCommandResult(typing.TypedDict, total=False): Success: bool Canceled: bool ErrorMessage: str - Result: str - ResultFormat: CommandResultFormat + Message: str + Data: CommandResultData class ResourceEventDto(typing.TypedDict, total=False): ResourceName: str diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index 8a31aa3be48..e25cea84082 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -323,6 +323,8 @@ pub enum CommandResultFormat { Text, #[serde(rename = "Json")] Json, + #[serde(rename = "Markdown")] + Markdown, } impl std::fmt::Display for CommandResultFormat { @@ -330,6 +332,7 @@ impl std::fmt::Display for CommandResultFormat { match self { Self::Text => write!(f, "Text"), Self::Json => write!(f, "Json"), + Self::Markdown => write!(f, "Markdown"), } } } @@ -512,10 +515,10 @@ pub struct ExecuteCommandResult { pub canceled: bool, #[serde(rename = "ErrorMessage")] pub error_message: String, - #[serde(rename = "Result")] - pub result: String, - #[serde(rename = "ResultFormat")] - pub result_format: CommandResultFormat, + #[serde(rename = "Message")] + pub message: String, + #[serde(rename = "Data")] + pub data: CommandResultData, } impl ExecuteCommandResult { @@ -524,8 +527,29 @@ impl ExecuteCommandResult { map.insert("Success".to_string(), serde_json::to_value(&self.success).unwrap_or(Value::Null)); map.insert("Canceled".to_string(), serde_json::to_value(&self.canceled).unwrap_or(Value::Null)); map.insert("ErrorMessage".to_string(), serde_json::to_value(&self.error_message).unwrap_or(Value::Null)); - map.insert("Result".to_string(), serde_json::to_value(&self.result).unwrap_or(Value::Null)); - map.insert("ResultFormat".to_string(), serde_json::to_value(&self.result_format).unwrap_or(Value::Null)); + map.insert("Message".to_string(), serde_json::to_value(&self.message).unwrap_or(Value::Null)); + map.insert("Data".to_string(), serde_json::to_value(&self.data).unwrap_or(Value::Null)); + map + } +} + +/// CommandResultData +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CommandResultData { + #[serde(rename = "Value")] + pub value: String, + #[serde(rename = "Format")] + pub format: CommandResultFormat, + #[serde(rename = "DisplayImmediately")] + pub display_immediately: bool, +} + +impl CommandResultData { + pub fn to_map(&self) -> HashMap { + let mut map = HashMap::new(); + map.insert("Value".to_string(), serde_json::to_value(&self.value).unwrap_or(Value::Null)); + map.insert("Format".to_string(), serde_json::to_value(&self.format).unwrap_or(Value::Null)); + map.insert("DisplayImmediately".to_string(), serde_json::to_value(&self.display_immediately).unwrap_or(Value::Null)); map } } diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index 03c76b21116..198f302fe4d 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -257,6 +257,7 @@ export enum CertificateTrustScope { export enum CommandResultFormat { Text = "Text", Json = "Json", + Markdown = "Markdown", } /** Enum type for ContainerLifetime */ @@ -382,6 +383,13 @@ export interface CommandOptions { updateState?: any; } +/** DTO interface for CommandResultData */ +export interface CommandResultData { + value?: string; + format?: CommandResultFormat; + displayImmediately?: boolean; +} + /** DTO interface for CreateBuilderOptions */ export interface CreateBuilderOptions { args?: string[]; @@ -399,8 +407,8 @@ export interface ExecuteCommandResult { success?: boolean; canceled?: boolean; errorMessage?: string; - result?: string; - resultFormat?: CommandResultFormat; + message?: string; + data?: CommandResultData; } /** DTO interface for ResourceEventDto */ diff --git a/tests/Aspire.Hosting.Tests/ResourceCommandServiceTests.cs b/tests/Aspire.Hosting.Tests/ResourceCommandServiceTests.cs index f3e1543841a..f00e9499815 100644 --- a/tests/Aspire.Hosting.Tests/ResourceCommandServiceTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceCommandServiceTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading.Channels; @@ -26,7 +26,7 @@ public async Task ExecuteCommandAsync_NoMatchingResource_Failure() // Assert Assert.False(result.Success); - Assert.Equal("Resource 'NotFoundResourceId' not found.", result.ErrorMessage); + Assert.Equal("Resource 'NotFoundResourceId' not found.", result.Message); } [Fact] @@ -49,7 +49,7 @@ public async Task ExecuteCommandAsync_ResourceNameMultipleMatches_Failure() // Assert Assert.False(result.Success); - Assert.Equal("Resource 'myResource' not found.", result.ErrorMessage); + Assert.Equal("Resource 'myResource' not found.", result.Message); } [Fact] @@ -68,7 +68,7 @@ public async Task ExecuteCommandAsync_NoMatchingCommand_Failure() // Assert Assert.False(result.Success); - Assert.Equal("Command 'NotFound' not available for resource 'myResource'.", result.ErrorMessage); + Assert.Equal("Command 'NotFound' not available for resource 'myResource'.", result.Message); } [Fact] @@ -165,7 +165,7 @@ public async Task ExecuteCommandAsync_HasReplicas_Failure_CalledPerReplica() displayName: "My command", executeCommand: e => { - return Task.FromResult(new ExecuteCommandResult { Success = false, ErrorMessage = "Failure!" }); + return Task.FromResult(new ExecuteCommandResult { Success = false, Message = "Failure!" }); }); // Act @@ -186,7 +186,7 @@ public async Task ExecuteCommandAsync_HasReplicas_Failure_CalledPerReplica() 2 command executions failed. Resource '{resourceNames[0]}' failed with error message: Failure! Resource '{resourceNames[1]}' failed with error message: Failure! - """, result.ErrorMessage); + """, result.Message); } [Fact] @@ -212,7 +212,7 @@ public async Task ExecuteCommandAsync_Canceled_Success() // Assert Assert.False(result.Success); Assert.True(result.Canceled); - Assert.Null(result.ErrorMessage); + Assert.Null(result.Message); } [Fact] @@ -242,7 +242,7 @@ public async Task ExecuteCommandAsync_HasReplicas_Canceled_CalledPerReplica() // Assert Assert.False(result.Success); Assert.True(result.Canceled); - Assert.Null(result.ErrorMessage); + Assert.Null(result.Message); } [Fact] @@ -285,7 +285,7 @@ public async Task ExecuteCommandAsync_HasReplicas_MixedFailureAndCanceled_OnlyFa Assert.Equal($""" 1 command executions failed. Resource '{resourceNames[0]}' failed with error message: Failure! - """, result.ErrorMessage); + """, result.Message); } [Fact] @@ -297,7 +297,7 @@ public void CommandResults_Canceled_ProducesCorrectResult() // Assert Assert.False(result.Success); Assert.True(result.Canceled); - Assert.Null(result.ErrorMessage); + Assert.Null(result.Message); } [Fact] @@ -323,7 +323,7 @@ public async Task ExecuteCommandAsync_OperationCanceledException_Canceled() // Assert Assert.False(result.Success); Assert.True(result.Canceled); - Assert.Null(result.ErrorMessage); + Assert.Null(result.Message); } [Fact] @@ -376,7 +376,7 @@ public async Task ExecuteCommandAsync_SuccessWithResult_ReturnsResultData() var custom = builder.AddResource(new CustomResource("myResource")); custom.WithCommand(name: "generate-token", displayName: "Generate Token", - executeCommand: _ => Task.FromResult(CommandResults.Success("{\"token\": \"abc123\"}", CommandResultFormat.Json))); + executeCommand: _ => Task.FromResult(CommandResults.Success("Generated token.", "{\"token\": \"abc123\"}", CommandResultFormat.Json))); var app = builder.Build(); await app.StartAsync(); @@ -384,8 +384,9 @@ public async Task ExecuteCommandAsync_SuccessWithResult_ReturnsResultData() var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "generate-token"); Assert.True(result.Success); - Assert.Equal("{\"token\": \"abc123\"}", result.Result); - Assert.Equal(CommandResultFormat.Json, result.ResultFormat); + Assert.NotNull(result.Data); + Assert.Equal("{\"token\": \"abc123\"}", result.Data.Value); + Assert.Equal(CommandResultFormat.Json, result.Data.Format); } [Fact] @@ -404,8 +405,7 @@ public async Task ExecuteCommandAsync_SuccessWithoutResult_ReturnsNoResultData() var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "mycommand"); Assert.True(result.Success); - Assert.Null(result.Result); - Assert.Null(result.ResultFormat); + Assert.Null(result.Data); } [Fact] @@ -424,7 +424,7 @@ public async Task ExecuteCommandAsync_HasReplicas_SuccessWithResult_ReturnsFirst executeCommand: e => { var count = Interlocked.Increment(ref callCount); - return Task.FromResult(CommandResults.Success($"token-{count}", CommandResultFormat.Text)); + return Task.FromResult(CommandResults.Success("Generated token.", $"token-{count}", CommandResultFormat.Text)); }); var app = builder.Build(); @@ -433,29 +433,31 @@ public async Task ExecuteCommandAsync_HasReplicas_SuccessWithResult_ReturnsFirst var result = await app.ResourceCommands.ExecuteCommandAsync(custom.Resource, "generate-token"); Assert.True(result.Success); - Assert.NotNull(result.Result); - Assert.StartsWith("token-", result.Result); - Assert.Equal(CommandResultFormat.Text, result.ResultFormat); + Assert.NotNull(result.Data); + Assert.StartsWith("token-", result.Data.Value); + Assert.Equal(CommandResultFormat.Text, result.Data.Format); } [Fact] public void CommandResults_SuccessWithResult_ProducesCorrectResult() { - var result = CommandResults.Success("{\"key\": \"value\"}", CommandResultFormat.Json); + var result = CommandResults.Success("Success.", "{\"key\": \"value\"}", CommandResultFormat.Json); Assert.True(result.Success); - Assert.Equal("{\"key\": \"value\"}", result.Result); - Assert.Equal(CommandResultFormat.Json, result.ResultFormat); + Assert.NotNull(result.Data); + Assert.Equal("{\"key\": \"value\"}", result.Data.Value); + Assert.Equal(CommandResultFormat.Json, result.Data.Format); } [Fact] public void CommandResults_SuccessWithTextResult_DefaultsToText() { - var result = CommandResults.Success("hello world"); + var result = CommandResults.Success("Success.", "hello world"); Assert.True(result.Success); - Assert.Equal("hello world", result.Result); - Assert.Equal(CommandResultFormat.Text, result.ResultFormat); + Assert.NotNull(result.Data); + Assert.Equal("hello world", result.Data.Value); + Assert.Equal(CommandResultFormat.Text, result.Data.Format); } private sealed class CustomResource(string name) : Resource(name), IResourceWithEndpoints, IResourceWithWaitSupport diff --git a/tests/Aspire.Hosting.Tests/WithHttpCommandTests.cs b/tests/Aspire.Hosting.Tests/WithHttpCommandTests.cs index 7e56739ec96..c123b83bdfd 100644 --- a/tests/Aspire.Hosting.Tests/WithHttpCommandTests.cs +++ b/tests/Aspire.Hosting.Tests/WithHttpCommandTests.cs @@ -397,7 +397,7 @@ public async Task WithHttpCommand_CallsGetResponseCallback_AfterSendingRequest() // Assert Assert.True(callbackCalled); Assert.False(result.Success); - Assert.Equal("A test error message", result.ErrorMessage); + Assert.Equal("A test error message", result.Message); } [Fact]