Skip to content
Merged
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
7 changes: 3 additions & 4 deletions docs/specs/with-terminal.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,10 +461,9 @@ The shortcut is suppressed while a terminal or text input has focus so it
does not consume typed input. Press <kbd>F6</kbd> first to move from terminal input
to its footer controls before toggling the dock.

The desktop header also has a terminal toggle button. On mobile, open the
navigation menu and select **Terminal** to open, collapse, or reopen the
dock without a keyboard. Both controls are available only for writable live runs
with the resource service enabled, and are hidden while switching runs.
On both desktop and mobile layouts, use the backtick shortcut to open or reopen
the dock. There is no terminal entry point in the desktop header or mobile
navigation menu.

When the dock is empty, it lists links to terminal-enabled resources on their
resource pages. Resource terminals remain separate from AppHost-owned dock tabs.
Expand Down
15 changes: 1 addition & 14 deletions src/Aspire.Dashboard/Components/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,6 @@
<FluentIcon Value="@(new Icons.Regular.Size24.BotSparkle())" Color="Color.Default"/>
</FluentButton>
}
@if (IsTerminalDockEnabled)
{
<FluentButton Class="header-button"
Appearance="ButtonAppearance.Default" OnClick="@ToggleTerminalDockAsync"
Title="@TerminalLoc[nameof(TerminalStrings.MainLayoutToggleTerminalDock)]" aria-label="@TerminalLoc[nameof(TerminalStrings.MainLayoutToggleTerminalDock)]">
@* WindowConsole only ships at Size20 in this Fluent version, so it is scaled to 24px to line up with
the rest of the header cluster. The Size24 alternatives (WindowDevTools, Code) read as "developer
tools" rather than "terminal". *@
<FluentIcon Value="@(new Icons.Regular.Size20.WindowConsole())" Width="24px" Color="Color.Default"/>
</FluentButton>
}
<NotificationsHeaderButton OnClick="@LaunchNotificationsAsync" />
<FluentButton Class="header-button"
Id="@SettingsButtonId"
Expand Down Expand Up @@ -135,8 +124,6 @@
LaunchHelpAsync="@LaunchHelpAsync"
LaunchAIAgentsAsync="@LaunchAIAgentsAsync"
IsAgentHelpEnabled="@(Options.CurrentValue.UI.DisableAgentHelp != true)"
IsTerminalDockEnabled="@IsTerminalDockEnabled"
ToggleTerminalDockAsync="@ToggleTerminalDockAsync"
LaunchNotificationsAsync="@LaunchNotificationsAsync"
LaunchSettingsAsync="@LaunchSettingsAsync" />
}
Expand Down Expand Up @@ -164,7 +151,7 @@
{
@* Removing the dock cancels its selected-run subscription and shortcut. Returning to the live run starts a
new subscription rather than reusing the stream chosen for a previous selection. *@
<TerminalDock @key="RunSelection.SelectedRun.RunId" @ref="_terminalDock" />
<TerminalDock @key="RunSelection.SelectedRun.RunId" />
}
<ResourceServiceConnectionProvider />
<div id="blazor-error-ui">
Expand Down
7 changes: 0 additions & 7 deletions src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ public partial class MainLayout : IGlobalKeydownListener, IAsyncDisposable
{
private bool _isNavMenuOpen;

private TerminalDock? _terminalDock;
private bool _runSelectionChanged;
private bool _isSwitchingRuns;
// Fluent v5 has no API to notify the provider after mutating an existing toast's options. This value is
Expand Down Expand Up @@ -66,9 +65,6 @@ public partial class MainLayout : IGlobalKeydownListener, IAsyncDisposable
[Inject]
public required IStringLocalizer<Resources.Layout> Loc { get; init; }

[Inject]
public required IStringLocalizer<Resources.TerminalStrings> TerminalLoc { get; init; }

[Inject]
public required DashboardDialogService DialogService { get; init; }

Expand Down Expand Up @@ -553,7 +549,4 @@ public async ValueTask DisposeAsync()
await JSInteropHelpers.SafeDisposeAsync(_jsModule);
await JSInteropHelpers.SafeDisposeAsync(_keyboardHandlers);
}

private Task ToggleTerminalDockAsync()
=> IsTerminalDockEnabled && _terminalDock is { } dock ? dock.ToggleAsync() : Task.CompletedTask;
}
18 changes: 0 additions & 18 deletions src/Aspire.Dashboard/Components/Layout/MobileNavMenu.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,6 @@ public partial class MobileNavMenu : ComponentBase, IAsyncDisposable
[Parameter, EditorRequired]
public required bool IsAgentHelpEnabled { get; set; }

[Parameter, EditorRequired]
public required bool IsTerminalDockEnabled { get; set; }

[Parameter, EditorRequired]
public required Func<Task> ToggleTerminalDockAsync { get; set; }

[Parameter, EditorRequired]
public required Func<Task> LaunchNotificationsAsync { get; set; }

Expand All @@ -56,9 +50,6 @@ public partial class MobileNavMenu : ComponentBase, IAsyncDisposable
[Inject]
public required IStringLocalizer<Resources.Layout> Loc { get; init; }

[Inject]
public required IStringLocalizer<Resources.TerminalStrings> TerminalLoc { get; init; }

[Inject]
public required IStringLocalizer<Resources.StructuredLogs> StructuredLogsLoc { get; init; }

Expand Down Expand Up @@ -214,15 +205,6 @@ private IEnumerable<MobileNavMenuEntry> GetMobileNavMenuEntries()
);
}

if (IsTerminalDockEnabled)
{
yield return new MobileNavMenuEntry(
TerminalLoc[nameof(Resources.TerminalStrings.TerminalTitle)],
ToggleTerminalDockAsync,
new Icons.Regular.Size20.WindowConsole()
);
}

yield return new MobileNavMenuEntry(
Loc[nameof(Resources.Layout.MainLayoutLaunchNotifications)],
LaunchNotificationsAsync,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,6 @@ public Task OnPageKeyDownAsync(AspireKeyboardShortcut shortcut)
/// <summary>
/// Shows the dock, or hides it if it is already showing.
/// </summary>
/// <remarks>
/// Public so the header button can drive the dock. The keyboard shortcut alone is not enough: <c>`</c> is
/// suppressed whenever focus is in a terminal or any other text input, because it types <c>`</c> there, so the
/// dock needs an affordance that works regardless of where focus happens to be.
/// </remarks>
public Task ToggleAsync() => InvokeAsync(() =>
{
if (_disposed || !DashboardClient.IsEnabled || DashboardClient.IsReadOnly)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.FluentUI.AspNetCore.Components;
using Xunit;

namespace Aspire.Dashboard.Components.Tests.Layout;
Expand Down Expand Up @@ -53,7 +52,7 @@ public async Task TerminalDock_RequiresResourceService(bool isEnabled, bool isDe
}

Assert.Equal(isEnabled ? 1 : 0, cut.FindComponents<TerminalDock>().Count);
Assert.Equal(isEnabled ? 1 : 0, cut.FindAll(toggleSelector).Count);
Assert.Empty(cut.FindAll(toggleSelector));
await cut.InvokeAsync(() => shortcuts.OnGlobalKeyDown(AspireKeyboardShortcut.ToggleTerminalDock));

if (isEnabled)
Expand All @@ -66,7 +65,7 @@ public async Task TerminalDock_RequiresResourceService(bool isEnabled, bool isDe
await cut.InvokeAsync(() => client.SetConnectionState(DashboardConnectionState.Disconnected));
cut.Render();
Assert.Same(dock, cut.FindComponent<TerminalDock>().Instance);
Assert.Single(cut.FindAll(toggleSelector));
Assert.Empty(cut.FindAll(toggleSelector));
await cut.InvokeAsync(() => dock.DisposeAsync().AsTask()).DefaultTimeout();
}
else
Expand Down Expand Up @@ -130,7 +129,7 @@ public async Task TerminalDock_RunSelection_OnlySubscribesWhileLive(bool startHi
}

var originalDock = cut.FindComponent<TerminalDock>().Instance;
Assert.Single(cut.FindAll(toggleSelector));
Assert.Empty(cut.FindAll(toggleSelector));
await updates.Writer.WriteAsync(TerminalSetupHelpers.Change(TerminalChangeType.Activated, "old"));
cut.WaitForAssertion(() =>
{
Expand All @@ -154,7 +153,7 @@ public async Task TerminalDock_RunSelection_OnlySubscribesWhileLive(bool startHi
await cut.InvokeAsync(() => cut.FindComponent<DashboardRunSelect>().Instance.SelectedRunIdChanged.InvokeAsync(null));
var newDock = cut.FindComponent<TerminalDock>().Instance;
Assert.NotSame(originalDock, newDock);
Assert.Single(cut.FindAll(toggleSelector));
Assert.Empty(cut.FindAll(toggleSelector));
await cut.InvokeAsync(() => shortcuts.OnGlobalKeyDown(AspireKeyboardShortcut.ToggleTerminalDock));
cut.WaitForAssertion(() =>
{
Expand All @@ -166,8 +165,10 @@ public async Task TerminalDock_RunSelection_OnlySubscribesWhileLive(bool startHi
await cut.InvokeAsync(() => newDock.DisposeAsync().AsTask()).DefaultTimeout();
}

[Fact]
public async Task TerminalDock_MobileMenu_CanOpenCollapseAndReopenWithoutKeyboard()
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task TerminalDock_Shortcut_CanOpenCollapseAndReopenWithoutNavigationEntry(bool isDesktop)
{
var client = new TestDashboardClient(
isEnabled: true,
Expand All @@ -178,25 +179,29 @@ public async Task TerminalDock_MobileMenu_CanOpenCollapseAndReopenWithoutKeyboar
SetupMainLayoutServices(dashboardClient: client);

var cut = Render<MainLayout>(builder => builder.Add(p => p.ViewportInformation,
new ViewportInformation(IsDesktop: false, IsUltraLowHeight: false, IsUltraLowWidth: false)));
var label = Services.GetRequiredService<IStringLocalizer<Resources.TerminalStrings>>()[nameof(Resources.TerminalStrings.TerminalTitle)].Value;
new ViewportInformation(IsDesktop: isDesktop, IsUltraLowHeight: false, IsUltraLowWidth: false)));
var label = Services.GetRequiredService<IStringLocalizer<Resources.TerminalStrings>>()[
isDesktop ? nameof(Resources.TerminalStrings.MainLayoutToggleTerminalDock) : nameof(Resources.TerminalStrings.TerminalTitle)].Value;
var toggleSelector = isDesktop ? $"header fluent-button[aria-label='{label}']" : $"fluent-menu-item[title='{label}']";
var shortcuts = Services.GetRequiredService<ShortcutManager>();
var dock = cut.FindComponent<TerminalDock>().Instance;
Assert.Empty(cut.FindAll(".terminal-dock"));

foreach (var visible in new[] { true, false, true })
if (!isDesktop)
{
await cut.InvokeAsync(() => cut.Find($"#{MainLayout.NavigationButtonId}").Click());
var item = cut.Find($"fluent-menu-item[title='{label}']");
Assert.Equal(label, item.TextContent.Trim());
Assert.Null(item.GetAttribute("aria-current"));
await cut.InvokeAsync(() => item.TriggerEvent("onmenuitemchange", new MenuItemEventArgs
{
Id = item.Id,
Text = item.TextContent
}));
Assert.Single(cut.FindAll(".mobile-nav-menu"));
Assert.Empty(cut.FindAll(toggleSelector));
await cut.InvokeAsync(() => cut.Find($"#{MainLayout.NavigationButtonId}").Click());
}

foreach (var visible in new[] { true, false, true })
{
await cut.InvokeAsync(() => shortcuts.OnGlobalKeyDown(AspireKeyboardShortcut.ToggleTerminalDock));

cut.WaitForAssertion(() =>
{
Assert.Empty(cut.FindAll(toggleSelector));
Assert.Empty(cut.FindAll(".mobile-nav-menu"));
var panel = Assert.Single(cut.FindAll(".terminal-dock"));
Assert.Equal(visible ? "false" : "true", panel.GetAttribute("aria-hidden"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,6 @@ private IRenderedComponent<MobileNavMenu> RenderMobileNavMenu(string currentUrl,
builder.Add(p => p.LaunchHelpAsync, () => Task.CompletedTask);
builder.Add(p => p.LaunchAIAgentsAsync, () => Task.CompletedTask);
builder.Add(p => p.IsAgentHelpEnabled, false);
builder.Add(p => p.IsTerminalDockEnabled, false);
builder.Add(p => p.ToggleTerminalDockAsync, () => Task.CompletedTask);
builder.Add(p => p.LaunchNotificationsAsync, () => Task.CompletedTask);
builder.Add(p => p.LaunchSettingsAsync, () => Task.CompletedTask);
});
Expand Down
Loading