diff --git a/Source/Cli/Commands/Chronicle/Workbench/views/ReadModelsView.cs b/Source/Cli/Commands/Chronicle/Workbench/views/ReadModelsView.cs index b304829..2a14b83 100644 --- a/Source/Cli/Commands/Chronicle/Workbench/views/ReadModelsView.cs +++ b/Source/Cli/Commands/Chronicle/Workbench/views/ReadModelsView.cs @@ -81,17 +81,42 @@ public void OpenDetailOverlay(WorkbenchReadModel rm) $"[{mut}]Queryable[/] [{queryableColor}]{(rm.IsQueryable ? "Yes" : "No")}[/]", $"[{mut}]Identifier[/] {rm.Identifier}"); - var instancesContent = BuildInstancesContent(rm); + // Open immediately with a placeholder for the Instances tab, then fetch off the UI thread so + // activating a row never blocks (or deadlocks) the render loop. The fetched content is pushed + // back into the tab editor on the UI thread once it arrives. + const string instancesTab = "Instances"; + var loadingContent = OnFetchInstances is null + ? $"[{mut}](No instance loader configured)[/]" + : $"[{mut}]Loading instances…[/]"; List<(string TabName, string Content)> tabs = [ ("Info", infoContent), - ("Instances", instancesContent) + (instancesTab, loadingContent) ]; var overlay = new DetailOverlayWindow(); var window = overlay.Build(_windowSystem, $" {rm.ContainerName} ", tabs, []); _windowSystem.AddWindow(window, activateWindow: true); + + if (OnFetchInstances is null) + { + return; + } + + var windowSystem = _windowSystem; + _ = Task.Run(async () => + { + var content = await FetchInstancesContentAsync(rm).ConfigureAwait(false); + windowSystem.EnqueueOnUIThread(() => + { + if (overlay.TabEditors.TryGetValue(instancesTab, out var editor)) + { + // The overlay strips markup to plain text for its read-only editors. + editor.SetContent(Markup.Remove(content)); + } + }); + }); } /// @@ -154,7 +179,7 @@ protected override IEnumerable GetCompletions(string input) => /// protected override void OnRowActivated(WorkbenchReadModel item) => OpenDetailOverlay(item); - string BuildInstancesContent(WorkbenchReadModel rm) + async Task FetchInstancesContentAsync(WorkbenchReadModel rm) { var mut = WorkbenchColors.Muted.ToMarkup(); var dan = WorkbenchColors.Danger.ToMarkup(); @@ -166,8 +191,8 @@ string BuildInstancesContent(WorkbenchReadModel rm) try { - var data = OnFetchInstances(rm.ContainerName, CancellationToken.None) - .GetAwaiter().GetResult(); + var data = await OnFetchInstances(rm.ContainerName, CancellationToken.None) + .ConfigureAwait(false); if (data.ReadModelInstancesError is not null) { diff --git a/Source/Cli/Commands/Chronicle/Workbench/windows/DetailOverlayWindow.cs b/Source/Cli/Commands/Chronicle/Workbench/windows/DetailOverlayWindow.cs index f0c7610..4c09edb 100644 --- a/Source/Cli/Commands/Chronicle/Workbench/windows/DetailOverlayWindow.cs +++ b/Source/Cli/Commands/Chronicle/Workbench/windows/DetailOverlayWindow.cs @@ -12,8 +12,15 @@ namespace Cratis.Cli.Commands.Chronicle.Workbench; /// public class DetailOverlayWindow { + readonly Dictionary _tabEditors = []; Window? _window; + /// + /// Gets the read-only editors backing each tab, keyed by tab name. Lets callers update a tab's + /// content after (for example, when a tab is populated by an async fetch). + /// + public IReadOnlyDictionary TabEditors => _tabEditors; + /// /// Builds a detail overlay window with the specified title, tabbed content, and action buttons. /// @@ -49,6 +56,7 @@ public Window Build( .Build(); tabBuilder.AddTab(tabName, editor); + _tabEditors[tabName] = editor; } var tabControl = tabBuilder.Fill().Build();