Skip to content

Commit bd23957

Browse files
authored
Resources graph (#7800)
1 parent 0161492 commit bd23957

File tree

17 files changed

+1135
-93
lines changed

17 files changed

+1135
-93
lines changed

src/Aspire.Dashboard/Components/Controls/Chart/ChartContainer.razor.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
::deep .tab-label > svg {
2-
margin-right: 2px;
2+
margin-right: calc(var(--design-unit) * 1px);
33
}
44

55
::deep .metric-tab {

src/Aspire.Dashboard/Components/Layout/DesktopNavMenu.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
{
1010
<FluentAppBarItem
1111
Href="@DashboardUrls.ResourcesUrl()"
12-
Match="NavLinkMatch.All"
12+
Match="@(_isResources ? NavLinkMatch.Prefix : NavLinkMatch.All)"
1313
IconRest="ResourcesIcon()"
1414
IconActive="ResourcesIcon(active: true)"
1515
Text="@Loc[nameof(Layout.NavMenuResourcesTab)]" />

src/Aspire.Dashboard/Components/Layout/DesktopNavMenu.razor.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Aspire.Dashboard.Utils;
45
using Microsoft.AspNetCore.Components;
6+
using Microsoft.AspNetCore.Components.Routing;
57
using Microsoft.FluentUI.AspNetCore.Components;
68
using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons;
79

810
namespace Aspire.Dashboard.Components.Layout;
911

10-
public partial class DesktopNavMenu : ComponentBase
12+
public partial class DesktopNavMenu : ComponentBase, IDisposable
1113
{
1214
internal static Icon ResourcesIcon(bool active = false) =>
1315
active ? new Icons.Filled.Size24.AppFolder()
@@ -28,4 +30,41 @@ internal static Icon TracesIcon(bool active = false) =>
2830
internal static Icon MetricsIcon(bool active = false) =>
2931
active ? new Icons.Filled.Size24.ChartMultiple()
3032
: new Icons.Regular.Size24.ChartMultiple();
33+
34+
[Inject]
35+
public required NavigationManager NavigationManager { get; init; }
36+
37+
// NavLink has limited options for matching the current address when highlighting itself as active.
38+
// Can't use Match.All because of the query string. Can't use Match.Prefix always because it matches every page.
39+
// Track whether we are on the resource page manually. If we are then change match to prefix to allow the query string.
40+
private bool _isResources;
41+
42+
protected override void OnInitialized()
43+
{
44+
NavigationManager.LocationChanged += OnLocationChanged;
45+
ProcessNavigationUri(NavigationManager.Uri);
46+
}
47+
48+
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
49+
{
50+
ProcessNavigationUri(e.Location);
51+
}
52+
53+
private void ProcessNavigationUri(string location)
54+
{
55+
if (Uri.TryCreate(location, UriKind.Absolute, out var result))
56+
{
57+
var isResources = result.AbsolutePath.TrimStart('/') == DashboardUrls.ResourcesBasePath;
58+
if (isResources != _isResources)
59+
{
60+
_isResources = isResources;
61+
StateHasChanged();
62+
}
63+
}
64+
}
65+
66+
public void Dispose()
67+
{
68+
NavigationManager.LocationChanged -= OnLocationChanged;
69+
}
3170
}

src/Aspire.Dashboard/Components/Pages/Resources.razor

Lines changed: 103 additions & 75 deletions
Large diffs are not rendered by default.

src/Aspire.Dashboard/Components/Pages/Resources.razor.cs

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Aspire.Dashboard.Components.Layout;
99
using Aspire.Dashboard.Extensions;
1010
using Aspire.Dashboard.Model;
11+
using Aspire.Dashboard.Model.ResourceGraph;
1112
using Aspire.Dashboard.Otlp.Storage;
1213
using Aspire.Dashboard.Utils;
1314
using Humanizer;
@@ -18,7 +19,7 @@
1819

1920
namespace Aspire.Dashboard.Components.Pages;
2021

21-
public partial class Resources : ComponentBase, IAsyncDisposable
22+
public partial class Resources : ComponentBase, IAsyncDisposable, IPageWithSessionAndUrlState<Resources.ResourcesViewModel, Resources.ResourcesPageState>
2223
{
2324
private const string TypeColumn = nameof(TypeColumn);
2425
private const string NameColumn = nameof(NameColumn);
@@ -47,6 +48,14 @@ public partial class Resources : ComponentBase, IAsyncDisposable
4748
[Inject]
4849
public required ISessionStorage SessionStorage { get; init; }
4950

51+
public string BasePath => DashboardUrls.ResourcesBasePath;
52+
public string SessionStorageKey => "Resources_PageState";
53+
public ResourcesViewModel PageViewModel { get; set; } = null!;
54+
55+
[Parameter]
56+
[SupplyParameterFromQuery(Name = "view")]
57+
public string? ViewKindName { get; set; }
58+
5059
[CascadingParameter]
5160
public required ViewportInformation ViewportInformation { get; set; }
5261

@@ -80,6 +89,8 @@ public partial class Resources : ComponentBase, IAsyncDisposable
8089
private GridColumnManager _manager = null!;
8190
private int _maxHighlightedCount;
8291
private readonly List<MenuButtonItem> _resourcesMenuItems = new();
92+
private DotNetObjectReference<ResourcesInterop>? _resourcesInteropReference;
93+
private IJSObjectReference? _jsModule;
8394
private AspirePageContentLayout? _contentLayout;
8495

8596
private ColumnResizeLabels _resizeLabels = ColumnResizeLabels.Default;
@@ -111,13 +122,15 @@ private async Task OnAllFilterVisibilityCheckedChangedAsync()
111122

112123
private async Task OnResourceFilterVisibilityChangedAsync(string resourceType, bool isVisible)
113124
{
125+
await UpdateResourceGraphResourcesAsync();
114126
await ClearSelectedResourceAsync();
115127
await _dataGrid.SafeRefreshDataAsync();
116128
UpdateMenuButtons();
117129
}
118130

119131
private async Task HandleSearchFilterChangedAsync()
120132
{
133+
await UpdateResourceGraphResourcesAsync();
121134
await ClearSelectedResourceAsync();
122135
await _dataGrid.SafeRefreshDataAsync();
123136
}
@@ -147,6 +160,11 @@ protected override async Task OnInitializedAsync()
147160
new GridColumn(Name: ActionsColumn, DesktopWidth: "minmax(150px, 1.5fr)", MobileWidth: "1fr")
148161
];
149162

163+
PageViewModel = new ResourcesViewModel
164+
{
165+
SelectedViewKind = ResourceViewKind.Table
166+
};
167+
150168
_applicationUnviewedErrorCounts = TelemetryRepository.GetApplicationUnviewedErrorLogsCount();
151169
UpdateMenuButtons();
152170

@@ -232,6 +250,7 @@ async Task SubscribeResourcesAsync()
232250
}
233251

234252
UpdateMaxHighlightedCount();
253+
await UpdateResourceGraphResourcesAsync();
235254
await InvokeAsync(async () =>
236255
{
237256
await _dataGrid.SafeRefreshDataAsync();
@@ -270,6 +289,47 @@ bool UpdateFromResource(ResourceViewModel resource, Func<string, bool> resourceT
270289
}
271290
}
272291

292+
protected override async Task OnAfterRenderAsync(bool firstRender)
293+
{
294+
if (PageViewModel.SelectedViewKind == ResourceViewKind.Graph && _jsModule == null)
295+
{
296+
_jsModule = await JS.InvokeAsync<IJSObjectReference>("import", "/js/app-resourcegraph.js");
297+
298+
_resourcesInteropReference = DotNetObjectReference.Create(new ResourcesInterop(this));
299+
300+
await _jsModule.InvokeVoidAsync("initializeResourcesGraph", _resourcesInteropReference);
301+
await UpdateResourceGraphResourcesAsync();
302+
}
303+
}
304+
305+
private async Task UpdateResourceGraphResourcesAsync()
306+
{
307+
if (PageViewModel.SelectedViewKind != ResourceViewKind.Graph || _jsModule == null)
308+
{
309+
return;
310+
}
311+
312+
var activeResources = _resourceByName.Values.Where(Filter).OrderBy(e => e.ResourceType).ThenBy(e => e.Name).ToList();
313+
var resources = activeResources.Select(r => ResourceGraphMapper.MapResource(r, _resourceByName, ColumnsLoc)).ToList();
314+
await _jsModule.InvokeVoidAsync("updateResourcesGraph", resources);
315+
}
316+
317+
private class ResourcesInterop(Resources resources)
318+
{
319+
[JSInvokable]
320+
public async Task SelectResource(string id)
321+
{
322+
if (resources._resourceByName.TryGetValue(id, out var resource))
323+
{
324+
await resources.InvokeAsync(async () =>
325+
{
326+
await resources.ShowResourceDetailsAsync(resource, null!);
327+
resources.StateHasChanged();
328+
});
329+
}
330+
}
331+
}
332+
273333
internal IEnumerable<ResourceViewModel> GetFilteredResources()
274334
{
275335
return _resourceByName
@@ -355,6 +415,11 @@ private void UpdateMaxHighlightedCount()
355415

356416
protected override async Task OnParametersSetAsync()
357417
{
418+
if (await this.InitializeViewModelAsync())
419+
{
420+
return;
421+
}
422+
358423
if (ResourceName is not null)
359424
{
360425
if (_resourceByName.TryGetValue(ResourceName, out var selectedResource))
@@ -423,6 +488,11 @@ private async Task ClearSelectedResourceAsync(bool causedByUserAction = false)
423488

424489
await InvokeAsync(StateHasChanged);
425490

491+
if (PageViewModel.SelectedViewKind == ResourceViewKind.Graph)
492+
{
493+
await UpdateResourceGraphSelectedAsync();
494+
}
495+
426496
if (_elementIdBeforeDetailsViewOpened is not null && causedByUserAction)
427497
{
428498
await JS.InvokeVoidAsync("focusElement", _elementIdBeforeDetailsViewOpened);
@@ -546,12 +616,88 @@ private bool HasAnyChildResources()
546616
return _resourceByName.Values.Any(r => !string.IsNullOrEmpty(r.GetResourcePropertyValue(KnownProperties.Resource.ParentName)));
547617
}
548618

619+
private Task OnTabChangeAsync(FluentTab newTab)
620+
{
621+
var id = newTab.Id?.Substring("tab-".Length);
622+
623+
if (id is null
624+
|| !Enum.TryParse(typeof(ResourceViewKind), id, out var o)
625+
|| o is not ResourceViewKind viewKind)
626+
{
627+
return Task.CompletedTask;
628+
}
629+
630+
return OnViewChangedAsync(viewKind);
631+
}
632+
633+
private async Task OnViewChangedAsync(ResourceViewKind newView)
634+
{
635+
PageViewModel.SelectedViewKind = newView;
636+
await this.AfterViewModelChangedAsync(_contentLayout, waitToApplyMobileChange: true);
637+
638+
if (newView == ResourceViewKind.Graph)
639+
{
640+
await UpdateResourceGraphResourcesAsync();
641+
await UpdateResourceGraphSelectedAsync();
642+
}
643+
}
644+
645+
private async Task UpdateResourceGraphSelectedAsync()
646+
{
647+
if (_jsModule != null)
648+
{
649+
await _jsModule.InvokeVoidAsync("updateResourcesGraphSelected", SelectedResource?.Name);
650+
}
651+
}
652+
653+
public sealed class ResourcesViewModel
654+
{
655+
public required ResourceViewKind SelectedViewKind { get; set; }
656+
}
657+
658+
public class ResourcesPageState
659+
{
660+
public required string? ViewKind { get; set; }
661+
}
662+
663+
public enum ResourceViewKind
664+
{
665+
Table,
666+
Graph
667+
}
668+
669+
public Task UpdateViewModelFromQueryAsync(ResourcesViewModel viewModel)
670+
{
671+
if (Enum.TryParse(typeof(ResourceViewKind), ViewKindName, out var view) && view is ResourceViewKind vk)
672+
{
673+
viewModel.SelectedViewKind = vk;
674+
}
675+
676+
return Task.CompletedTask;
677+
}
678+
679+
public string GetUrlFromSerializableViewModel(ResourcesPageState serializable)
680+
{
681+
return DashboardUrls.ResourcesUrl(view: serializable.ViewKind);
682+
}
683+
684+
public ResourcesPageState ConvertViewModelToSerializable()
685+
{
686+
return new ResourcesPageState
687+
{
688+
ViewKind = (PageViewModel.SelectedViewKind != ResourceViewKind.Table) ? PageViewModel.SelectedViewKind.ToString() : null
689+
};
690+
}
691+
549692
public async ValueTask DisposeAsync()
550693
{
694+
_resourcesInteropReference?.Dispose();
551695
_watchTaskCancellationTokenSource.Cancel();
552696
_watchTaskCancellationTokenSource.Dispose();
553697
_logsSubscription?.Dispose();
554698

699+
await JSInteropHelpers.SafeDisposeAsync(_jsModule);
700+
555701
await TaskHelpers.WaitIgnoreCancelAsync(_resourceSubscriptionTask);
556702
}
557703
}

0 commit comments

Comments
 (0)