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
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ private void ProcessNavigationUri(string location)
{
if (Uri.TryCreate(location, UriKind.Absolute, out var result))
{
var isResources = result.AbsolutePath.TrimStart('/') == DashboardUrls.ResourcesBasePath;
var trimmedPath = result.AbsolutePath.TrimStart('/');
var isResources = trimmedPath == DashboardUrls.ResourcesBasePath || trimmedPath[0] == '?';
if (isResources != _isResources)
{
_isResources = isResources;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ private IEnumerable<MobileNavMenuEntry> GetMobileNavMenuEntries()
Loc[nameof(Resources.Layout.NavMenuResourcesTab)],
() => NavigateToAsync(DashboardUrls.ResourcesUrl()),
DesktopNavMenu.ResourcesIcon(),
LinkMatchRegex: new Regex($"^{DashboardUrls.ResourcesUrl()}$")
LinkMatchRegex: new Regex($"^{DashboardUrls.ResourcesUrl()}(\\?.*)?$")
);

yield return new MobileNavMenuEntry(
Expand Down
12 changes: 6 additions & 6 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@
}

<div>
<ResourceFilters ResourceStates="@ResourceStatesToVisibility"
ResourceTypes="@ResourceTypesToVisibility"
ResourceHealthStates="@ResourceHealthStatusesToVisibility"
<ResourceFilters ResourceStates="@PageViewModel.ResourceStatesToVisibility"
ResourceTypes="@PageViewModel.ResourceTypesToVisibility"
ResourceHealthStates="@PageViewModel.ResourceHealthStatusesToVisibility"
OnAllFilterVisibilityCheckedChangedAsync="@OnAllFilterVisibilityCheckedChangedAsync"
OnResourceFilterVisibilityChangedAsync="@OnResourceFilterVisibilityChangedAsync" />
</div>
Expand All @@ -79,9 +79,9 @@
FixedPlacement="true"
Class="resources-filter-popup">
<Body>
<ResourceFilters ResourceStates="@ResourceStatesToVisibility"
ResourceTypes="@ResourceTypesToVisibility"
ResourceHealthStates="@ResourceHealthStatusesToVisibility"
<ResourceFilters ResourceStates="@PageViewModel.ResourceStatesToVisibility"
ResourceTypes="@PageViewModel.ResourceTypesToVisibility"
ResourceHealthStates="@PageViewModel.ResourceHealthStatusesToVisibility"
OnAllFilterVisibilityCheckedChangedAsync="@OnAllFilterVisibilityCheckedChangedAsync"
OnResourceFilterVisibilityChangedAsync="@OnResourceFilterVisibilityChangedAsync" />
</Body>
Expand Down
119 changes: 73 additions & 46 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Aspire.Dashboard.Model.ResourceGraph;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Utils;
using Aspire.Hosting.Utils;
using Humanizer;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -54,7 +55,7 @@ public partial class Resources : ComponentBase, IAsyncDisposable, IPageWithSessi
public required IOptionsMonitor<DashboardOptions> DashboardOptions { get; init; }

public string BasePath => DashboardUrls.ResourcesBasePath;
public string SessionStorageKey => "Resources_PageState";
public string SessionStorageKey => BrowserStorageKeys.ResourcesPageState;
public ResourcesViewModel PageViewModel { get; set; } = null!;

[Parameter]
Expand All @@ -66,15 +67,15 @@ public partial class Resources : ComponentBase, IAsyncDisposable, IPageWithSessi

[Parameter]
[SupplyParameterFromQuery]
public string? VisibleTypes { get; set; }
public string? HiddenTypes { get; set; }

[Parameter]
[SupplyParameterFromQuery]
public string? VisibleStates { get; set; }
public string? HiddenStates { get; set; }

[Parameter]
[SupplyParameterFromQuery]
public string? VisibleHealthStates { get; set; }
public string? HiddenHealthStates { get; set; }

[Parameter]
[SupplyParameterFromQuery(Name = "resource")]
Expand Down Expand Up @@ -108,17 +109,11 @@ public partial class Resources : ComponentBase, IAsyncDisposable, IPageWithSessi
private ColumnSortLabels _sortLabels = ColumnSortLabels.Default;
private bool _showResourceTypeColumn;

// Filters in the resource popup
// Internal for tests
internal ConcurrentDictionary<string, bool> ResourceTypesToVisibility { get; } = new(StringComparers.ResourceName);
internal ConcurrentDictionary<string, bool> ResourceStatesToVisibility { get; } = new(StringComparers.ResourceState);
internal ConcurrentDictionary<string, bool> ResourceHealthStatusesToVisibility { get; } = new(StringComparer.Ordinal);

private bool Filter(ResourceViewModel resource)
{
return IsKeyValueTrue(resource.ResourceType, ResourceTypesToVisibility)
&& IsKeyValueTrue(resource.State ?? string.Empty, ResourceStatesToVisibility)
&& IsKeyValueTrue(resource.HealthStatus?.Humanize() ?? string.Empty, ResourceHealthStatusesToVisibility)
return IsKeyValueTrue(resource.ResourceType, PageViewModel.ResourceTypesToVisibility)
&& IsKeyValueTrue(resource.State ?? string.Empty, PageViewModel.ResourceStatesToVisibility)
&& IsKeyValueTrue(resource.HealthStatus?.Humanize() ?? string.Empty, PageViewModel.ResourceHealthStatusesToVisibility)
&& (_filter.Length == 0 || resource.MatchesFilter(_filter))
&& !resource.IsHiddenState();

Expand All @@ -130,6 +125,7 @@ private async Task OnAllFilterVisibilityCheckedChangedAsync()
await ClearSelectedResourceAsync();
await _dataGrid.SafeRefreshDataAsync();
UpdateMenuButtons();
await this.AfterViewModelChangedAsync(_contentLayout, waitToApplyMobileChange: false);
}

private async Task OnResourceFilterVisibilityChangedAsync(string resourceType, bool isVisible)
Expand All @@ -138,6 +134,7 @@ private async Task OnResourceFilterVisibilityChangedAsync(string resourceType, b
await ClearSelectedResourceAsync();
await _dataGrid.SafeRefreshDataAsync();
UpdateMenuButtons();
await this.AfterViewModelChangedAsync(_contentLayout, waitToApplyMobileChange: false);
}

private async Task HandleSearchFilterChangedAsync()
Expand All @@ -149,9 +146,9 @@ private async Task HandleSearchFilterChangedAsync()

// Internal for tests
internal bool NoFiltersSet => AreAllTypesVisible && AreAllStatesVisible && AreAllHealthStatesVisible;
internal bool AreAllTypesVisible => ResourceTypesToVisibility.Values.All(value => value);
internal bool AreAllStatesVisible => ResourceStatesToVisibility.Values.All(value => value);
internal bool AreAllHealthStatesVisible => ResourceHealthStatusesToVisibility.Values.All(value => value);
internal bool AreAllTypesVisible => PageViewModel.ResourceTypesToVisibility.Values.All(value => value);
internal bool AreAllStatesVisible => PageViewModel.ResourceStatesToVisibility.Values.All(value => value);
internal bool AreAllHealthStatesVisible => PageViewModel.ResourceHealthStatusesToVisibility.Values.All(value => value);

private readonly GridSort<ResourceGridViewModel> _nameSort = GridSort<ResourceGridViewModel>.ByAscending(p => p.Resource, ResourceViewModelNameComparer.Instance);
private readonly GridSort<ResourceGridViewModel> _stateSort = GridSort<ResourceGridViewModel>.ByAscending(p => p.Resource.State).ThenAscending(p => p.Resource, ResourceViewModelNameComparer.Instance);
Expand Down Expand Up @@ -218,21 +215,12 @@ protected override async Task OnInitializedAsync()

async Task SubscribeResourcesAsync()
{
var preselectedVisibleResourceTypes = VisibleTypes?.Split(',').ToHashSet();
var preselectedVisibleResourceStates = VisibleStates?.Split(',').ToHashSet();
var preselectedVisibleResourceHealthStates = VisibleHealthStates?.Split(',').ToHashSet();

var (snapshot, subscription) = await DashboardClient.SubscribeResourcesAsync(_watchTaskCancellationTokenSource.Token);

// Apply snapshot.
foreach (var resource in snapshot)
{
var added = UpdateFromResource(
resource,
type => preselectedVisibleResourceTypes is null || preselectedVisibleResourceTypes.Contains(type),
state => preselectedVisibleResourceStates is null || preselectedVisibleResourceStates.Contains(state),
healthStatus => preselectedVisibleResourceHealthStates is null || preselectedVisibleResourceHealthStates.Contains(healthStatus));

var added = UpdateFromResource(resource);
Debug.Assert(added, "Should not receive duplicate resources in initial snapshot data.");
}

Expand Down Expand Up @@ -284,29 +272,42 @@ await InvokeAsync(async () =>
}
});
}
}

private bool UpdateFromResource(ResourceViewModel resource)
{
var preselectedHiddenResourceTypes = HiddenTypes?.Split(' ').Select(StringUtils.Unescape).ToHashSet();
var preselectedHiddenResourceStates = HiddenStates?.Split(' ').Select(StringUtils.Unescape).ToHashSet();
var preselectedHiddenResourceHealthStates = HiddenHealthStates?.Split(' ').Select(StringUtils.Unescape).ToHashSet();

bool UpdateFromResource(ResourceViewModel resource, Func<string, bool> resourceTypeVisible, Func<string, bool> stateVisible, Func<string, bool> healthStatusVisible)
return UpdateFromResource(
resource,
type => preselectedHiddenResourceTypes is null || !preselectedHiddenResourceTypes.Contains(type),
state => preselectedHiddenResourceStates is null || !preselectedHiddenResourceStates.Contains(state),
healthStatus => preselectedHiddenResourceHealthStates is null || !preselectedHiddenResourceHealthStates.Contains(healthStatus));
}

private bool UpdateFromResource(ResourceViewModel resource, Func<string, bool> resourceTypeVisible, Func<string, bool> stateVisible, Func<string, bool> healthStatusVisible)
{
// This is ok from threadsafty perspective because we are the only thread that's modifying resources.
bool added;
if (_resourceByName.TryGetValue(resource.Name, out _))
{
// This is ok from threadsafty perspective because we are the only thread that's modifying resources.
bool added;
if (_resourceByName.TryGetValue(resource.Name, out _))
{
added = false;
_resourceByName[resource.Name] = resource;
}
else
{
added = _resourceByName.TryAdd(resource.Name, resource);
}
added = false;
_resourceByName[resource.Name] = resource;
}
else
{
added = _resourceByName.TryAdd(resource.Name, resource);
}

ResourceTypesToVisibility.TryAdd(resource.ResourceType, resourceTypeVisible(resource.ResourceType));
ResourceStatesToVisibility.TryAdd(resource.State ?? string.Empty, stateVisible(resource.State ?? string.Empty));
ResourceHealthStatusesToVisibility.TryAdd(resource.HealthStatus?.Humanize() ?? string.Empty, healthStatusVisible(resource.HealthStatus?.Humanize() ?? string.Empty));
PageViewModel.ResourceTypesToVisibility.AddOrUpdate(resource.ResourceType, resourceTypeVisible(resource.ResourceType), (_, _) => resourceTypeVisible(resource.ResourceType));
PageViewModel.ResourceStatesToVisibility.AddOrUpdate(resource.State ?? string.Empty, stateVisible(resource.State ?? string.Empty), (_, _) => stateVisible(resource.State ?? string.Empty));
PageViewModel.ResourceHealthStatusesToVisibility.AddOrUpdate(resource.HealthStatus?.Humanize() ?? string.Empty, healthStatusVisible(resource.HealthStatus?.Humanize() ?? string.Empty), (_, _) => healthStatusVisible(resource.HealthStatus?.Humanize() ?? string.Empty));

UpdateMenuButtons();
UpdateMenuButtons();

return added;
}
return added;
}

protected override async Task OnAfterRenderAsync(bool firstRender)
Expand Down Expand Up @@ -476,6 +477,12 @@ protected override async Task OnParametersSetAsync()
return;
}

// If filters were saved in page state, resource filters now need to be recomputed since the URL has changed.
foreach (var resourceViewModel in _resourceByName)
{
UpdateFromResource(resourceViewModel.Value);
}

if (ResourceName is not null)
{
if (_resourceByName.TryGetValue(ResourceName, out var selectedResource))
Expand Down Expand Up @@ -757,11 +764,17 @@ private async Task UpdateResourceGraphSelectedAsync()
public sealed class ResourcesViewModel
{
public required ResourceViewKind SelectedViewKind { get; set; }
public ConcurrentDictionary<string, bool> ResourceTypesToVisibility { get; } = new(StringComparers.ResourceName);
public ConcurrentDictionary<string, bool> ResourceStatesToVisibility { get; } = new(StringComparers.ResourceState);
public ConcurrentDictionary<string, bool> ResourceHealthStatusesToVisibility { get; } = new(StringComparer.Ordinal);
}

public class ResourcesPageState
{
public required string? ViewKind { get; set; }
public required IDictionary<string, bool> ResourceTypesToVisibility { get; set; }
public required IDictionary<string, bool> ResourceStatesToVisibility { get; set; }
public required IDictionary<string, bool> ResourceHealthStatusesToVisibility { get; set; }
}

public enum ResourceViewKind
Expand All @@ -783,14 +796,28 @@ public Task UpdateViewModelFromQueryAsync(ResourcesViewModel viewModel)

public string GetUrlFromSerializableViewModel(ResourcesPageState serializable)
{
return DashboardUrls.ResourcesUrl(view: serializable.ViewKind);
return DashboardUrls.ResourcesUrl(
view: serializable.ViewKind,
// add resource?
hiddenTypes: SerializeFiltersToString(serializable.ResourceTypesToVisibility),
hiddenStates: SerializeFiltersToString(serializable.ResourceStatesToVisibility),
hiddenHealthStates: SerializeFiltersToString(serializable.ResourceHealthStatusesToVisibility));

static string? SerializeFiltersToString(IDictionary<string, bool> filters)
{
var escapedFilters = filters.Where(kvp => !kvp.Value).Select(kvp => StringUtils.Escape(kvp.Key)).ToList();
return escapedFilters.Count == 0 ? null : string.Join(" ", escapedFilters);
}
}

public ResourcesPageState ConvertViewModelToSerializable()
{
return new ResourcesPageState
{
ViewKind = (PageViewModel.SelectedViewKind != ResourceViewKind.Table) ? PageViewModel.SelectedViewKind.ToString() : null
ViewKind = PageViewModel.SelectedViewKind != ResourceViewKind.Table ? PageViewModel.SelectedViewKind.ToString() : null,
ResourceTypesToVisibility = PageViewModel.ResourceTypesToVisibility,
ResourceStatesToVisibility = PageViewModel.ResourceStatesToVisibility,
ResourceHealthStatusesToVisibility = PageViewModel.ResourceHealthStatusesToVisibility
};
}

Expand Down
18 changes: 4 additions & 14 deletions src/Aspire.Dashboard/Extensions/TelemetryFilterFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Web;
using Aspire.Dashboard.Model.Otlp;
using Aspire.Hosting.Utils;

namespace Aspire.Dashboard.Extensions;

Expand All @@ -25,7 +25,7 @@ private static string SerializeFilterToString(TelemetryFilter filter)
_ => null
};

var filterString = $"{Escape(filter.Field)}:{condition}:{Escape(filter.Value)}";
var filterString = $"{StringUtils.Escape(filter.Field)}:{condition}:{StringUtils.Escape(filter.Value)}";
if (!filter.Enabled)
{
filterString += $":{DisabledText}";
Expand All @@ -47,7 +47,7 @@ public static string SerializeFiltersToString(IEnumerable<TelemetryFilter> filte
return null;
}

var field = Unescape(parts[0]);
var field = StringUtils.Unescape(parts[0]);

FilterCondition? condition = parts[1] switch
{
Expand All @@ -67,7 +67,7 @@ public static string SerializeFiltersToString(IEnumerable<TelemetryFilter> filte
return null;
}

var value = Unescape(parts[2]);
var value = StringUtils.Unescape(parts[2]);

var enabled = parts is not [_, _, _, DisabledText];

Expand All @@ -80,16 +80,6 @@ public static string SerializeFiltersToString(IEnumerable<TelemetryFilter> filte
};
}

private static string Escape(string value)
{
return HttpUtility.UrlEncode(value);
}

private static string Unescape(string value)
{
return HttpUtility.UrlDecode(value);
}

public static List<TelemetryFilter> DeserializeFiltersFromString(string filtersString)
{
return filtersString
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Dashboard/Utils/BrowserStorageKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal static class BrowserStorageKeys
public const string StructuredLogsPageState = "Aspire_PageState_StructuredLogs";
public const string MetricsPageState = "Aspire_PageState_Metrics";
public const string ConsoleLogsPageState = "Aspire_PageState_ConsoleLogs";
public const string ResourcesPageState = "Resources_PageState";
public const string ConsoleLogConsoleSettings = "Aspire_ConsoleLog_ConsoleSettings";
public const string ConsoleLogFilters = "Aspire_ConsoleLog_Filters";
public const string ResourcesCollapsedResourceNames = "Aspire_Resources_CollapsedResourceNames";
Expand Down
14 changes: 13 additions & 1 deletion src/Aspire.Dashboard/Utils/DashboardUrls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal static class DashboardUrls
public const string StructuredLogsBasePath = "structuredlogs";
public const string TracesBasePath = "traces";

public static string ResourcesUrl(string? resource = null, string? view = null)
public static string ResourcesUrl(string? resource = null, string? view = null, string? hiddenTypes = null, string? hiddenStates = null, string? hiddenHealthStates = null)
{
var url = $"/{ResourcesBasePath}";
if (resource != null)
Expand All @@ -25,6 +25,18 @@ public static string ResourcesUrl(string? resource = null, string? view = null)
{
url = QueryHelpers.AddQueryString(url, "view", view);
}
if (hiddenTypes != null)
{
url = QueryHelpers.AddQueryString(url, "hiddenTypes", hiddenTypes);
}
if (hiddenStates != null)
{
url = QueryHelpers.AddQueryString(url, "hiddenStates", hiddenStates);
}
if (hiddenHealthStates != null)
{
url = QueryHelpers.AddQueryString(url, "hiddenHealthStates", hiddenHealthStates);
}

return url;
}
Expand Down
11 changes: 11 additions & 0 deletions src/Shared/StringUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Web;

namespace Aspire.Hosting.Utils;

Expand All @@ -21,4 +22,14 @@ public static bool TryGetUriFromDelimitedString([NotNullWhen(true)] string? inpu
return false;
}
}

public static string Escape(string value)
{
return HttpUtility.UrlEncode(value);
}

public static string Unescape(string value)
{
return HttpUtility.UrlDecode(value);
}
}
Loading