|
8 | 8 | using Aspire.Dashboard.Components.Layout;
|
9 | 9 | using Aspire.Dashboard.Extensions;
|
10 | 10 | using Aspire.Dashboard.Model;
|
| 11 | +using Aspire.Dashboard.Model.ResourceGraph; |
11 | 12 | using Aspire.Dashboard.Otlp.Storage;
|
12 | 13 | using Aspire.Dashboard.Utils;
|
13 | 14 | using Humanizer;
|
|
18 | 19 |
|
19 | 20 | namespace Aspire.Dashboard.Components.Pages;
|
20 | 21 |
|
21 |
| -public partial class Resources : ComponentBase, IAsyncDisposable |
| 22 | +public partial class Resources : ComponentBase, IAsyncDisposable, IPageWithSessionAndUrlState<Resources.ResourcesViewModel, Resources.ResourcesPageState> |
22 | 23 | {
|
23 | 24 | private const string TypeColumn = nameof(TypeColumn);
|
24 | 25 | private const string NameColumn = nameof(NameColumn);
|
@@ -47,6 +48,14 @@ public partial class Resources : ComponentBase, IAsyncDisposable
|
47 | 48 | [Inject]
|
48 | 49 | public required ISessionStorage SessionStorage { get; init; }
|
49 | 50 |
|
| 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 | + |
50 | 59 | [CascadingParameter]
|
51 | 60 | public required ViewportInformation ViewportInformation { get; set; }
|
52 | 61 |
|
@@ -80,6 +89,8 @@ public partial class Resources : ComponentBase, IAsyncDisposable
|
80 | 89 | private GridColumnManager _manager = null!;
|
81 | 90 | private int _maxHighlightedCount;
|
82 | 91 | private readonly List<MenuButtonItem> _resourcesMenuItems = new();
|
| 92 | + private DotNetObjectReference<ResourcesInterop>? _resourcesInteropReference; |
| 93 | + private IJSObjectReference? _jsModule; |
83 | 94 | private AspirePageContentLayout? _contentLayout;
|
84 | 95 |
|
85 | 96 | private ColumnResizeLabels _resizeLabels = ColumnResizeLabels.Default;
|
@@ -111,13 +122,15 @@ private async Task OnAllFilterVisibilityCheckedChangedAsync()
|
111 | 122 |
|
112 | 123 | private async Task OnResourceFilterVisibilityChangedAsync(string resourceType, bool isVisible)
|
113 | 124 | {
|
| 125 | + await UpdateResourceGraphResourcesAsync(); |
114 | 126 | await ClearSelectedResourceAsync();
|
115 | 127 | await _dataGrid.SafeRefreshDataAsync();
|
116 | 128 | UpdateMenuButtons();
|
117 | 129 | }
|
118 | 130 |
|
119 | 131 | private async Task HandleSearchFilterChangedAsync()
|
120 | 132 | {
|
| 133 | + await UpdateResourceGraphResourcesAsync(); |
121 | 134 | await ClearSelectedResourceAsync();
|
122 | 135 | await _dataGrid.SafeRefreshDataAsync();
|
123 | 136 | }
|
@@ -147,6 +160,11 @@ protected override async Task OnInitializedAsync()
|
147 | 160 | new GridColumn(Name: ActionsColumn, DesktopWidth: "minmax(150px, 1.5fr)", MobileWidth: "1fr")
|
148 | 161 | ];
|
149 | 162 |
|
| 163 | + PageViewModel = new ResourcesViewModel |
| 164 | + { |
| 165 | + SelectedViewKind = ResourceViewKind.Table |
| 166 | + }; |
| 167 | + |
150 | 168 | _applicationUnviewedErrorCounts = TelemetryRepository.GetApplicationUnviewedErrorLogsCount();
|
151 | 169 | UpdateMenuButtons();
|
152 | 170 |
|
@@ -232,6 +250,7 @@ async Task SubscribeResourcesAsync()
|
232 | 250 | }
|
233 | 251 |
|
234 | 252 | UpdateMaxHighlightedCount();
|
| 253 | + await UpdateResourceGraphResourcesAsync(); |
235 | 254 | await InvokeAsync(async () =>
|
236 | 255 | {
|
237 | 256 | await _dataGrid.SafeRefreshDataAsync();
|
@@ -270,6 +289,47 @@ bool UpdateFromResource(ResourceViewModel resource, Func<string, bool> resourceT
|
270 | 289 | }
|
271 | 290 | }
|
272 | 291 |
|
| 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 | + |
273 | 333 | internal IEnumerable<ResourceViewModel> GetFilteredResources()
|
274 | 334 | {
|
275 | 335 | return _resourceByName
|
@@ -355,6 +415,11 @@ private void UpdateMaxHighlightedCount()
|
355 | 415 |
|
356 | 416 | protected override async Task OnParametersSetAsync()
|
357 | 417 | {
|
| 418 | + if (await this.InitializeViewModelAsync()) |
| 419 | + { |
| 420 | + return; |
| 421 | + } |
| 422 | + |
358 | 423 | if (ResourceName is not null)
|
359 | 424 | {
|
360 | 425 | if (_resourceByName.TryGetValue(ResourceName, out var selectedResource))
|
@@ -423,6 +488,11 @@ private async Task ClearSelectedResourceAsync(bool causedByUserAction = false)
|
423 | 488 |
|
424 | 489 | await InvokeAsync(StateHasChanged);
|
425 | 490 |
|
| 491 | + if (PageViewModel.SelectedViewKind == ResourceViewKind.Graph) |
| 492 | + { |
| 493 | + await UpdateResourceGraphSelectedAsync(); |
| 494 | + } |
| 495 | + |
426 | 496 | if (_elementIdBeforeDetailsViewOpened is not null && causedByUserAction)
|
427 | 497 | {
|
428 | 498 | await JS.InvokeVoidAsync("focusElement", _elementIdBeforeDetailsViewOpened);
|
@@ -546,12 +616,88 @@ private bool HasAnyChildResources()
|
546 | 616 | return _resourceByName.Values.Any(r => !string.IsNullOrEmpty(r.GetResourcePropertyValue(KnownProperties.Resource.ParentName)));
|
547 | 617 | }
|
548 | 618 |
|
| 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 | + |
549 | 692 | public async ValueTask DisposeAsync()
|
550 | 693 | {
|
| 694 | + _resourcesInteropReference?.Dispose(); |
551 | 695 | _watchTaskCancellationTokenSource.Cancel();
|
552 | 696 | _watchTaskCancellationTokenSource.Dispose();
|
553 | 697 | _logsSubscription?.Dispose();
|
554 | 698 |
|
| 699 | + await JSInteropHelpers.SafeDisposeAsync(_jsModule); |
| 700 | + |
555 | 701 | await TaskHelpers.WaitIgnoreCancelAsync(_resourceSubscriptionTask);
|
556 | 702 | }
|
557 | 703 | }
|
0 commit comments