Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/Aspire.Dashboard/Model/DashboardCommandExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,20 @@ public async Task ExecuteAsyncCore(ResourceViewModel resource, CommandViewModel
}
else if (response.Kind == ResourceCommandResponseKind.Cancelled)
{
var canceledTitle = string.Format(CultureInfo.InvariantCulture, loc[nameof(Dashboard.Resources.Resources.ResourceCommandCanceled)], command.GetDisplayName());

// 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);
notificationService.ReplaceNotification(progressNotificationId, new NotificationEntry
{
Title = canceledTitle,
Body = response.Message,
Intent = FluentMessageIntent.Warning,
});
closeToastCts.Dispose();
return;
}
Expand Down
9 changes: 9 additions & 0 deletions src/Aspire.Dashboard/Resources/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/Aspire.Dashboard/Resources/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@
<data name="ResourcesDetailsStateProperty" xml:space="preserve">
<value>State</value>
</data>
<data name="ResourceCommandCanceled" xml:space="preserve">
<value>"{0}" canceled</value>
<comment>{0} is the display name of the command.</comment>
</data>
<data name="ResourceCommandFailed" xml:space="preserve">
<value>"{0}" failed</value>
<comment>{0} is the display name of the command.</comment>
Expand Down
5 changes: 5 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Resources.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Resources.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Resources.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Resources.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Resources.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Resources.ja.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Resources.ko.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Resources.pl.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Resources.pt-BR.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Resources.ru.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Resources.tr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hans.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Resources.zh-Hant.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

188 changes: 188 additions & 0 deletions tests/Aspire.Dashboard.Tests/Model/DashboardCommandExecutorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// 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.Components.Resize;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Resources;
using Aspire.Dashboard.Telemetry;
using Aspire.Dashboard.Tests.Shared;
using Aspire.Tests.Shared.DashboardModel;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.FluentUI.AspNetCore.Components;
using Xunit;

namespace Aspire.Dashboard.Tests.Model;

public sealed class DashboardCommandExecutorTests
{
[Fact]
public async Task ExecuteAsync_CancelledCommand_NotificationRemainsWithWarningIntent()
{
var notificationService = new TestNotificationService();
var dashboardClient = new TestDashboardClient(
isEnabled: true,
executeResourceCommand: (_, _, _, _, _) => Task.FromResult(new ResourceCommandResponseViewModel
{
Kind = ResourceCommandResponseKind.Cancelled,
Message = "Operation was canceled"
}));

var executor = CreateExecutor(dashboardClient, notificationService);
var resource = ModelTestHelpers.CreateResource(resourceName: "test-resource", state: KnownResourceState.Running);
var command = new CommandViewModel("stop", CommandViewModelState.Enabled, "Stop", "Stop the resource", confirmationMessage: "", argumentInputs: [], isHighlighted: false, iconName: string.Empty, iconVariant: IconVariant.Regular);

await executor.ExecuteAsync(resource, command, r => r.Name);

var notifications = notificationService.GetNotifications();
var notification = Assert.Single(notifications);
Assert.Equal(MessageIntent.Warning, notification.Entry.Intent);
Assert.Contains("Stop", notification.Entry.Title);
Comment thread
JamesNK marked this conversation as resolved.
Outdated
}

[Fact]
public async Task ExecuteAsync_SucceededCommand_NotificationHasSuccessIntent()
{
var notificationService = new TestNotificationService();
var dashboardClient = new TestDashboardClient(
isEnabled: true,
executeResourceCommand: (_, _, _, _, _) => Task.FromResult(new ResourceCommandResponseViewModel
{
Kind = ResourceCommandResponseKind.Succeeded,
Message = "Done"
}));

var executor = CreateExecutor(dashboardClient, notificationService);
var resource = ModelTestHelpers.CreateResource(resourceName: "test-resource", state: KnownResourceState.Running);
var command = new CommandViewModel("start", CommandViewModelState.Enabled, "Start", "Start the resource", confirmationMessage: "", argumentInputs: [], isHighlighted: false, iconName: string.Empty, iconVariant: IconVariant.Regular);

await executor.ExecuteAsync(resource, command, r => r.Name);

var notifications = notificationService.GetNotifications();
var notification = Assert.Single(notifications);
Assert.Equal(MessageIntent.Success, notification.Entry.Intent);
}

[Fact]
public async Task ExecuteAsync_FailedCommand_NotificationHasErrorIntent()
{
var notificationService = new TestNotificationService();
var dashboardClient = new TestDashboardClient(
isEnabled: true,
executeResourceCommand: (_, _, _, _, _) => Task.FromResult(new ResourceCommandResponseViewModel
{
Kind = ResourceCommandResponseKind.Failed,
ErrorMessage = "Something went wrong"
}));

var executor = CreateExecutor(dashboardClient, notificationService);
var resource = ModelTestHelpers.CreateResource(resourceName: "test-resource", state: KnownResourceState.Running);
var command = new CommandViewModel("restart", CommandViewModelState.Enabled, "Restart", "Restart the resource", confirmationMessage: "", argumentInputs: [], isHighlighted: false, iconName: string.Empty, iconVariant: IconVariant.Regular);

await executor.ExecuteAsync(resource, command, r => r.Name);

var notifications = notificationService.GetNotifications();
var notification = Assert.Single(notifications);
Assert.Equal(MessageIntent.Error, notification.Entry.Intent);
}

private static DashboardCommandExecutor CreateExecutor(IDashboardClient dashboardClient, INotificationService notificationService)
{
var dimensionManager = new DimensionManager();
dimensionManager.InvokeOnViewportInformationChanged(new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));

var telemetrySender = new TestDashboardTelemetrySender();
var telemetryService = new DashboardTelemetryService(NullLogger<DashboardTelemetryService>.Instance, telemetrySender);

var dialogService = new DashboardDialogService(
new TestDialogService(),
new TestStringLocalizer<Dialogs>(),
dimensionManager);

return new DashboardCommandExecutor(
dashboardClient,
dialogService,
new TestToastService(),
new TestStringLocalizer<Resources.Resources>(),
new TestNavigationManager(),
telemetryService,
notificationService);
}

private sealed class TestNavigationManager : NavigationManager
{
public TestNavigationManager()
{
Initialize("http://localhost/", "http://localhost/");
}
}

private sealed class TestToastService : IToastService
{
public event Action<string?>? OnClose;
public event Action? OnUpdate;
public event Action? OnClearAll;

public void ClearAll() { }
public void CloseToast(string id) => OnClose?.Invoke(id);
public void ShowCommunicationToast(ToastParameters<CommunicationToastContent> parameters) { }
public void ShowConfirmationToast(ToastParameters<ConfirmationToastContent> parameters) { }
public void ShowProgressToast(ToastParameters<ProgressToastContent> parameters) { }
public void ShowToast(Type? component, ToastParameters parameters) { }
public void UpdateToast(string id, ToastParameters parameters) => OnUpdate?.Invoke();
}

private sealed class TestNotificationService : INotificationService
{
private readonly List<(string Id, NotificationEntry Entry)> _notifications = [];
private int _nextId;

public int UnreadCount => _notifications.Count;

public event Action? OnChange;

public string AddNotification(NotificationEntry notification)
{
var id = (++_nextId).ToString();
notification.Timestamp = DateTimeOffset.UtcNow;
_notifications.Add((id, notification));
OnChange?.Invoke();
return id;
}

public void ReplaceNotification(string id, NotificationEntry notification)
{
notification.Timestamp = DateTimeOffset.UtcNow;
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)
{
_notifications.RemoveAll(n => n.Id == id);
OnChange?.Invoke();
}

public void ClearAll()
{
_notifications.Clear();
OnChange?.Invoke();
}

public void ResetUnreadCount() { }

public IReadOnlyList<NotificationMessage> GetNotifications()
{
return _notifications.Select(n => new NotificationMessage { Id = n.Id, Entry = n.Entry }).ToList();
}
}
}
Loading