File: Components\Dialogs\FilterDialog.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 Aspire.Dashboard.Model;
using Aspire.Dashboard.Model.Otlp;
using Aspire.Dashboard.Otlp.Storage;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.FluentUI.AspNetCore.Components;
 
namespace Aspire.Dashboard.Components.Dialogs;
 
public partial class FilterDialog
{
    private List<SelectViewModel<FilterCondition>> _filterConditions = null!;
 
    private SelectViewModel<FilterCondition> CreateFilterSelectViewModel(FilterCondition condition) =>
        new SelectViewModel<FilterCondition> { Id = condition, Name = TelemetryFilter.ConditionToString(condition, FilterLoc) };
 
    [CascadingParameter]
    public FluentDialog? Dialog { get; set; }
 
    [Parameter]
    public FilterDialogViewModel Content { get; set; } = default!;
 
    [Inject]
    public required TelemetryRepository TelemetryRepository { get; init; }
 
    private FilterDialogFormModel _formModel = default!;
    private List<SelectViewModel<string>> _parameters = default!;
    private List<SelectViewModel<FieldValue>> _filteredValues = default!;
    private List<SelectViewModel<FieldValue>>? _allValues;
 
    public EditContext EditContext { get; private set; } = default!;
 
    protected override void OnInitialized()
    {
        _filterConditions =
        [
            CreateFilterSelectViewModel(FilterCondition.Equals),
            CreateFilterSelectViewModel(FilterCondition.Contains),
            CreateFilterSelectViewModel(FilterCondition.NotEqual),
            CreateFilterSelectViewModel(FilterCondition.NotContains)
        ];
 
        _formModel = new FilterDialogFormModel();
        EditContext = new EditContext(_formModel);
 
        _filteredValues = [];
    }
 
    protected override void OnParametersSet()
    {
        var knownFields = Content.KnownKeys.Select(p => new SelectViewModel<string> { Id = p, Name = TelemetryFilter.ResolveFieldName(p) }).ToList();
        var customFields = Content.PropertyKeys.Select(p => new SelectViewModel<string> { Id = p, Name = TelemetryFilter.ResolveFieldName(p) }).ToList();
 
        if (customFields.Count > 0)
        {
            _parameters =
            [
                .. knownFields,
                new SelectViewModel<string> { Id = null, Name = "-" },
                .. customFields
            ];
        }
        else
        {
            _parameters = knownFields;
        }
 
        if (Content.Filter is { } filter)
        {
            _formModel.Parameter = _parameters.SingleOrDefault(c => c.Id == filter.Field);
            _formModel.Condition = _filterConditions.Single(c => c.Id == filter.Condition);
            _formModel.Value = filter.Value;
        }
        else
        {
            _formModel.Parameter = _parameters.FirstOrDefault();
            _formModel.Condition = _filterConditions.Single(c => c.Id == FilterCondition.Contains);
            _formModel.Value = "";
        }
 
        UpdateParameterFieldValues();
        ValueChanged();
    }
 
    private void UpdateParameterFieldValues()
    {
        if (_formModel.Parameter?.Id is { } parameterName)
        {
            var fieldValues = Content.GetFieldValues(parameterName);
            _allValues = fieldValues
                .Select(kvp => new FieldValue { Value = kvp.Key, Count = kvp.Value })
                .OrderByDescending(v => v.Count)
                .ThenBy(v => v.Value, StringComparers.OtlpFieldValue)
                .Select(v => new SelectViewModel<FieldValue> { Id = v, Name = v.Value })
                .ToList();
        }
        else
        {
            _allValues = null;
        }
    }
 
    private async Task ParameterChangedAsync()
    {
        UpdateParameterFieldValues();
 
        _formModel.Value = "";
        StateHasChanged();
 
        // Clearing the selected value and the combo box items together wasn't correctly clearing the selected value.
        // This is hacky, but adding a delay between the two operations puts the combo box in the right state.
        // Limitation of FluentUI: https://github.com/microsoft/fluentui-blazor/issues/2708
        await Task.Delay(100);
        ValueChanged();
    }
 
    // There is a bug in FluentUI that prevents the value changing immediately. Will be fixed in a future FluentUI update.
    // https://github.com/microsoft/fluentui-blazor/issues/2672
    private void ValueChanged()
    {
        if (_allValues != null)
        {
            IEnumerable<SelectViewModel<FieldValue>> newValues = _allValues;
            if (_formModel.Value is { Length: > 0 } value)
            {
                newValues = newValues.Where(vm => vm.Name.Contains(value, StringComparison.OrdinalIgnoreCase));
            }
 
            // Limit to 1000 items to avoid the combo box have too many items and impacting UI perf.
            _filteredValues = newValues.Take(1000).ToList();
        }
        else
        {
            _filteredValues = [];
        }
    }
 
    private void Cancel()
    {
        Dialog!.CancelAsync();
    }
 
    private void Delete()
    {
        Dialog!.CloseAsync(DialogResult.Ok(new FilterDialogResult() { Filter = Content.Filter, Delete = true }));
    }
 
    private void Apply()
    {
        if (Content.Filter is { } filter)
        {
            filter.Field = _formModel.Parameter!.Id!;
            filter.Condition = _formModel.Condition!.Id;
            filter.Value = _formModel.Value!;
 
            Dialog!.CloseAsync(DialogResult.Ok(new FilterDialogResult() { Filter = filter, Delete = false }));
        }
        else
        {
            filter = new TelemetryFilter
            {
                Field = _formModel.Parameter!.Id!,
                Condition = _formModel.Condition!.Id,
                Value = _formModel.Value!
            };
 
            Dialog!.CloseAsync(DialogResult.Ok(new FilterDialogResult() { Filter = filter, Add = true }));
        }
    }
 
    private sealed class FieldValue
    {
        public required string Value { get; init; }
        public required int Count { get; init; }
    }
}