File: Components\Controls\ResourcePaneFilter.razor.cs
Web Access
Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Concurrent;
using Aspire.Dashboard.Model;
using Microsoft.AspNetCore.Components;
 
namespace Aspire.Dashboard.Components;
 
public partial class ResourcePaneFilter
{
    // Types and states are what most filtering needs, so they start open. Health and tags open on their own once
    // they filter something. The user's choice to open or close a group wins after that.
    private static readonly string[] s_defaultExpandedGroups = ["types", "states"];
 
    private readonly Dictionary<string, bool> _expandedOverrides = new(StringComparer.Ordinal);
    private bool _isOpen;
 
    /// <summary>
    /// A set of values the pane can be filtered by, such as the resource types.
    /// </summary>
    public sealed record FilterGroup(
        string Id,
        string Header,
        ConcurrentDictionary<string, bool> Values,
        Func<ResourceViewModel, IEnumerable<string>> GetKeys,
        Func<string, string>? GetText = null);
 
    [Parameter, EditorRequired]
    public required string ButtonId { get; set; }
 
    [Parameter, EditorRequired]
    public required string Title { get; set; }
 
    [Parameter, EditorRequired]
    public required Func<IReadOnlyList<FilterGroup>> Groups { get; set; }
 
    /// <summary>
    /// Gets or sets the resources that value counts are taken from, whatever the other filters hide.
    /// </summary>
    [Parameter, EditorRequired]
    public required Func<IEnumerable<ResourceViewModel>> Resources { get; set; }
 
    /// <summary>
    /// Raised after a filter value changes, so the pane applies it.
    /// </summary>
    [Parameter, EditorRequired]
    public required EventCallback OnChanged { get; set; }
 
    private bool IsFiltered => Groups().Any(group => group.Values.Values.Any(isVisible => !isVisible));
 
    private void TogglePopover() => _isOpen = !_isOpen;
 
    private void Close() => _isOpen = false;
 
    private void OnOpenedChanged(bool opened) => _isOpen = opened;
 
    private bool IsExpanded(FilterGroup group, List<KeyValuePair<string, bool>> options) =>
        _expandedOverrides.TryGetValue(group.Id, out var expanded)
            ? expanded
            : s_defaultExpandedGroups.Contains(group.Id) || options.Any(pair => !pair.Value);
 
    private void ToggleGroup(string groupId, bool isExpanded) => _expandedOverrides[groupId] = !isExpanded;
 
    private Dictionary<string, Dictionary<string, int>> GetCounts(IReadOnlyList<FilterGroup> groups)
    {
        // Each value shows how many resources it matches, whatever the other filters, so hiding a value doesn't change
        // the counts. The keys are those the filter itself uses, so the counts use the dictionary's comparer.
        var resources = Resources().ToList();
        var result = new Dictionary<string, Dictionary<string, int>>(StringComparer.Ordinal);
        foreach (var group in groups)
        {
            var counts = new Dictionary<string, int>(group.Values.Comparer);
            foreach (var resource in resources)
            {
                foreach (var key in group.GetKeys(resource))
                {
                    counts[key] = counts.GetValueOrDefault(key) + 1;
                }
            }
 
            result[group.Id] = counts;
        }
 
        return result;
    }
 
    private async Task SetValueAsync(FilterGroup group, string key, bool isVisible)
    {
        group.Values[key] = isVisible;
        await OnChanged.InvokeAsync();
    }
 
    private async Task SetAllAsync(FilterGroup group, bool isVisible)
    {
        foreach (var key in group.Values.Keys)
        {
            group.Values[key] = isVisible;
        }
 
        await OnChanged.InvokeAsync();
    }
 
    private async Task ResetAsync()
    {
        foreach (var group in Groups())
        {
            foreach (var key in group.Values.Keys)
            {
                group.Values[key] = true;
            }
        }
 
        await OnChanged.InvokeAsync();
    }
}