Skip to content

Gracefully handle connection error between dashboard and app host #10880

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions src/Aspire.Dashboard/Components/Controls/ApplicationName.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,28 @@ protected override async Task OnInitializedAsync()
if (DashboardClient.IsEnabled && !DashboardClient.WhenConnected.IsCompletedSuccessfully)
{
_disposalCts = new CancellationTokenSource();
await DashboardClient.WhenConnected.WaitAsync(_disposalCts.Token);
try
{
await DashboardClient.WhenConnected.WaitAsync(_disposalCts.Token);
}
catch
{
// Ignore exceptions that occur during connection. Other code handles this error.
_pageTitle = GetApplicationName();
}
}
}

protected override void OnParametersSet()
{
var applicationName = GetApplicationName();

_pageTitle = string.IsNullOrEmpty(AdditionalText)
? applicationName
: $"{applicationName} ({AdditionalText})";
}

private string GetApplicationName()
{
string applicationName;

Expand All @@ -48,9 +65,7 @@ protected override void OnParametersSet()
applicationName = DashboardClient.ApplicationName;
}

_pageTitle = string.IsNullOrEmpty(AdditionalText)
? applicationName
: $"{applicationName} ({AdditionalText})";
return applicationName;
}

public void Dispose()
Expand Down
53 changes: 53 additions & 0 deletions src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using Aspire.Dashboard.Components.Dialogs;
using Aspire.Dashboard.Components.Pages;
using Aspire.Dashboard.Configuration;
Expand All @@ -25,6 +26,7 @@ public partial class MainLayout : IGlobalKeydownListener, IAsyncDisposable
private DotNetObjectReference<ShortcutManager>? _shortcutManagerReference;
private DotNetObjectReference<MainLayout>? _layoutReference;
private IDialogReference? _openPageDialog;
private CancellationTokenSource? _disposalCts;

private const string SettingsDialogId = "SettingsDialog";
private const string HelpDialogId = "HelpDialog";
Expand Down Expand Up @@ -68,6 +70,9 @@ public partial class MainLayout : IGlobalKeydownListener, IAsyncDisposable
[Inject]
public required ILocalStorage LocalStorage { get; init; }

[Inject]
public required ILogger<MainLayout> Logger { get; init; }

[CascadingParameter]
public required ViewportInformation ViewportInformation { get; set; }

Expand Down Expand Up @@ -100,6 +105,15 @@ protected override async Task OnInitializedAsync()
return ValueTask.CompletedTask;
});
}
else
{
if (!DashboardClient.WhenConnected.IsCompletedSuccessfully)
{
// Check if the dashboard client can connect.
// Run in a new task to not block other behavior if a connection error occurs and a dialog is shown.
_ = Task.Run(EnsureClientConnectionAsync);
}
}

var result = await JS.InvokeAsync<BrowserInfo>("window.getBrowserInfo");
TimeProvider.SetBrowserTimeZone(result.TimeZone);
Expand Down Expand Up @@ -136,6 +150,44 @@ await MessageService.ShowMessageBarAsync(options =>
}
}

private async Task EnsureClientConnectionAsync()
{
Debug.Assert(DashboardClient.IsEnabled, "Dashboard client must be enabled to ensure connection.");

_disposalCts = new CancellationTokenSource();
try
{
await DashboardClient.WhenConnected.WaitAsync(_disposalCts.Token);
}
catch (Exception ex)
{
Logger.LogError(ex, "Connection error.");

var dialogParameters = new DialogParameters<MessageBoxContent>
{
PrimaryAction = DialogsLoc[nameof(Resources.Dialogs.ConnectionErrorDialogRetryButton)],
SecondaryAction = DialogsLoc[nameof(Resources.Dialogs.ConnectionErrorDialogCancelButton)],
PreventDismissOnOverlayClick = true,
Content = new MessageBoxContent
{
Title = DialogsLoc[nameof(Resources.Dialogs.ConnectionErrorDialogTitle)],
MarkupMessage = new MarkupString(DialogsLoc[nameof(Resources.Dialogs.ConnectionErrorDialogMessage)]),
Intent = MessageBoxIntent.Confirmation,
IconColor = Color.Error,
Icon = new Microsoft.FluentUI.AspNetCore.Components.Icons.Filled.Size24.DismissCircle()
}
};
var reference = await DialogService.ShowMessageBoxAsync(dialogParameters);

var result = await reference.Result;
if (!result.Cancelled)
{
// Force dashboard to reload.
NavigationManager.NavigateTo(DashboardUrls.ResourcesUrl(), forceLoad: true);
}
}
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
Expand Down Expand Up @@ -280,6 +332,7 @@ private void CloseMobileNavMenu()

public async ValueTask DisposeAsync()
{
_disposalCts?.Cancel();
_shortcutManagerReference?.Dispose();
_layoutReference?.Dispose();
_themeChangedSubscription?.Dispose();
Expand Down
36 changes: 36 additions & 0 deletions src/Aspire.Dashboard/Resources/Dialogs.Designer.cs

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

12 changes: 12 additions & 0 deletions src/Aspire.Dashboard/Resources/Dialogs.resx
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,16 @@
<data name="InteractionButtonClose" xml:space="preserve">
<value>Close</value>
</data>
<data name="ConnectionErrorDialogTitle" xml:space="preserve">
<value>Connection error</value>
</data>
<data name="ConnectionErrorDialogMessage" xml:space="preserve">
<value>Unable to connect to the app host. Please ensure the app host is running and try again.</value>
</data>
<data name="ConnectionErrorDialogRetryButton" xml:space="preserve">
<value>Retry</value>
</data>
<data name="ConnectionErrorDialogCancelButton" xml:space="preserve">
<value>Cancel</value>
</data>
</root>
20 changes: 20 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Dialogs.cs.xlf

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading
Loading