Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release/8.0] Add tooltips to Resource table #3615

Merged
merged 2 commits into from
Apr 11, 2024
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
24 changes: 16 additions & 8 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
@page "/"
@using Aspire.Dashboard.Components.ResourcesGridColumns
@using Aspire.Dashboard.Model
@using Aspire.Dashboard.Resources
@using Aspire.Dashboard.Utils
@using System.Globalization
@using Humanizer
@inject IStringLocalizer<Dashboard.Resources.Resources> Loc
@inject IStringLocalizer<ControlsStrings> ControlsStringsLoc
@inject IStringLocalizer<Columns> ColumnsLoc

<PageTitle><ApplicationName ResourceName="@nameof(Dashboard.Resources.Resources.ResourcesPageTitle)" Loc="@Loc" /></PageTitle>

Expand Down Expand Up @@ -57,21 +58,28 @@
}
<FluentDataGrid Virtualize="true" GenerateHeader="GenerateHeaderOption.Sticky" ItemSize="46" Items="@FilteredResources" ResizableColumns="true" GridTemplateColumns="@gridTemplateColumns" RowClass="GetRowClass" Loading="_isLoading">
<ChildContent>
<PropertyColumn Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesTypeColumnHeader)]" Property="@(c => c.ResourceType)" Sortable="true" />
<TemplateColumn Title="@ControlsStringsLoc[nameof(ControlsStrings.NameColumnHeader)]" Sortable="true" SortBy="@_nameSort">
<PropertyColumn Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesTypeColumnHeader)]" Property="@(c => c.ResourceType)" Sortable="true" Tooltip="true" TooltipText="@(c => c.ResourceType)"/>
<TemplateColumn Title="@ControlsStringsLoc[nameof(ControlsStrings.NameColumnHeader)]" Sortable="true" SortBy="@_nameSort" Tooltip="true" TooltipText="@(c => GetResourceName(c))" >
<ResourceNameDisplay Resource="context" FilterText="@_filter" FormatName="GetResourceName" />
</TemplateColumn>
<TemplateColumn Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesStateColumnHeader)]" Sortable="true" SortBy="@_stateSort">
<TemplateColumn Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesStateColumnHeader)]" Sortable="true" SortBy="@_stateSort" Tooltip="true" TooltipText="@(c => c.State.Humanize())">
<StateColumnDisplay Resource="@context" UnviewedErrorCounts ="@_applicationUnviewedErrorCounts" />
</TemplateColumn>
<TemplateColumn Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesStartTimeColumnHeader)]" Sortable="true" SortBy="@_startTimeSort" TooltipText="@(context => context.CreationTimeStamp != null ? FormatHelpers.FormatDateTime(TimeProvider, context.CreationTimeStamp.Value, MillisecondsDisplay.None, CultureInfo.CurrentCulture) : null)" Tooltip="true">
<StartTimeColumnDisplay Resource="@context" />
</TemplateColumn>
<TemplateColumn Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesSourceColumnHeader)]">
<SourceColumnDisplay Resource="context" FilterText="@_filter" />
<TemplateColumn Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesSourceColumnHeader)]" Tooltip="true" TooltipText="@(ctx => GetSourceColumnValueAndTooltip(ctx)?.Tooltip)">
@if (GetSourceColumnValueAndTooltip(context) is { } columnDisplay)
{
<SourceColumnDisplay Resource="context" FilterText="@_filter" Value="@columnDisplay.Value" ContentAfterValue="@columnDisplay.ContentAfterValue" ValueToCopy="@columnDisplay.ValueToCopy" Tooltip="@columnDisplay.Tooltip" />
}
</TemplateColumn>
<TemplateColumn Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesEndpointsColumnHeader)]">
<EndpointsColumnDisplay Resource="context" HasMultipleReplicas="HasMultipleReplicas(context)" />
<TemplateColumn Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesEndpointsColumnHeader)]" Tooltip="true" TooltipText="@(ctx => GetEndpointsTooltip(ctx))">
<EndpointsColumnDisplay
Resource="context"
HasMultipleReplicas="HasMultipleReplicas(context)"
DisplayedEndpoints="@GetDisplayedEndpoints(context, out var additionalMessage)"
AdditionalMessage="@additionalMessage" />
</TemplateColumn>
<TemplateColumn Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesLogsColumnHeader)]" Class="no-ellipsis">
<FluentAnchor Appearance="Appearance.Lightweight" Href="@DashboardUrls.ConsoleLogsUrl(resource: context.Name)">@ControlsStringsLoc[ControlsStrings.ViewAction]</FluentAnchor>
Expand Down
82 changes: 82 additions & 0 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Resources;
using Aspire.Dashboard.Utils;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
Expand Down Expand Up @@ -294,6 +296,86 @@ private async Task ExecuteResourceCommandAsync(ResourceViewModel resource, Comma
}
}

private static (string Value, string? ContentAfterValue, string ValueToCopy, string Tooltip)? GetSourceColumnValueAndTooltip(ResourceViewModel resource)
{
// NOTE projects are also executables, so we have to check for projects first
if (resource.IsProject() && resource.TryGetProjectPath(out var projectPath))
{
return (Value: Path.GetFileName(projectPath), ContentAfterValue: null, ValueToCopy: projectPath, Tooltip: projectPath);
}

if (resource.TryGetExecutablePath(out var executablePath))
{
resource.TryGetExecutableArguments(out var arguments);
var argumentsString = arguments.IsDefaultOrEmpty ? "" : string.Join(" ", arguments);
var fullCommandLine = $"{executablePath} {argumentsString}";

return (Value: Path.GetFileName(executablePath), ContentAfterValue: argumentsString, ValueToCopy: fullCommandLine, Tooltip: fullCommandLine);
}

if (resource.TryGetContainerImage(out var containerImage))
{
return (Value: containerImage, ContentAfterValue: null, ValueToCopy: containerImage, Tooltip: containerImage);
}

if (resource.Properties.TryGetValue(KnownProperties.Resource.Source, out var value) && value.HasStringValue)
{
return (Value: value.StringValue, ContentAfterValue: null, ValueToCopy: value.StringValue, Tooltip: value.StringValue);
}

return null;
}

private string GetEndpointsTooltip(ResourceViewModel resource)
{
var displayedEndpoints = GetDisplayedEndpoints(resource, out var additionalMessage);

if (additionalMessage is not null)
{
return additionalMessage;
}

if (displayedEndpoints.Count == 1)
{
return displayedEndpoints.First().Text;
}

var maxShownEndpoints = 3;
var tooltipBuilder = new StringBuilder(string.Join(", ", displayedEndpoints.Take(maxShownEndpoints).Select(endpoint => endpoint.Text)));

if (displayedEndpoints.Count > maxShownEndpoints)
{
tooltipBuilder.Append(CultureInfo.CurrentCulture, $" + {displayedEndpoints.Count - maxShownEndpoints}");
}

return tooltipBuilder.ToString();
}

private List<DisplayedEndpoint> GetDisplayedEndpoints(ResourceViewModel resource, out string? additionalMessage)
{
if (resource.Urls.Length == 0)
{
// If we have no endpoints, and the app isn't running anymore or we're not expecting any, then just say None
additionalMessage = ColumnsLoc[nameof(Columns.EndpointsColumnDisplayNone)];
return [];
}

additionalMessage = null;

// Make sure that endpoints have a consistent ordering. Show https first, then everything else.
return [.. GetEndpoints(resource)
.OrderByDescending(e => e.Url?.StartsWith("https") == true)
.ThenBy(e=> e.Url ?? e.Text)];
}

/// <summary>
/// A resource has services and endpoints. These can overlap. This method attempts to return a single list without duplicates.
/// </summary>
private static List<DisplayedEndpoint> GetEndpoints(ResourceViewModel resource)
{
return ResourceEndpointHelpers.GetEndpoints(resource, includeInteralUrls: false);
}

public async ValueTask DisposeAsync()
{
_watchTaskCancellationTokenSource.Cancel();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,9 @@
@using Aspire.Dashboard.Model
@using Aspire.Dashboard.Resources
@namespace Aspire.Dashboard.Components
@inject IStringLocalizer<Columns> Loc

@{
List<DisplayedEndpoint> displayedEndpoints;
string? additionalMessage = null;
if (Resource.Urls.Length == 0)
{
// If we have no endpoints, and the app isn't running anymore or we're not expecting any, then just say None
additionalMessage = Loc[nameof(Columns.EndpointsColumnDisplayNone)];
displayedEndpoints = [];
}
else
{
displayedEndpoints = GetEndpoints(Resource);
}
}

@if (displayedEndpoints.Count == 1)
@if (DisplayedEndpoints.Count == 1)
{
var displayedEndpoint = displayedEndpoints[0];
var displayedEndpoint = DisplayedEndpoints[0];
if (displayedEndpoint.Url != null)
{
<a href="@displayedEndpoint.Url" target="_blank">@displayedEndpoint.Text</a>
Expand All @@ -32,15 +15,12 @@
}
else
{
// Make sure that endpoints have a consistent ordering. Show https first, then everything else.
displayedEndpoints = displayedEndpoints.OrderByDescending(e => e.Url?.StartsWith("https") == true).ThenBy(e=> e.Url ?? e.Text).ToList();

<FluentOverflow Class="endpoint-overflow">
<ChildContent>
@for (var i = 0; i < displayedEndpoints.Count; i++)
@for (var i = 0; i < DisplayedEndpoints.Count; i++)
{
var displayedEndpoint = displayedEndpoints[i];
var isLast = i == displayedEndpoints.Count - 1;
var displayedEndpoint = DisplayedEndpoints[i];
var isLast = i == DisplayedEndpoints.Count - 1;

<FluentOverflowItem Data="displayedEndpoint">
@if (displayedEndpoint.Url != null)
Expand Down Expand Up @@ -94,7 +74,7 @@ else
</FluentOverflow>
}

@if (!string.IsNullOrEmpty(additionalMessage))
@if (!string.IsNullOrEmpty(AdditionalMessage))
{
<div>@additionalMessage</div>
<div>@AdditionalMessage</div>
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,14 @@ public partial class EndpointsColumnDisplay
[Parameter, EditorRequired]
public required bool HasMultipleReplicas { get; set; }

[Parameter, EditorRequired]
public required IList<DisplayedEndpoint> DisplayedEndpoints { get; set; }

[Parameter]
public string? AdditionalMessage { get; set; }

[Inject]
public required ILogger<EndpointsColumnDisplay> Logger { get; init; }

private bool _popoverVisible;

/// <summary>
/// A resource has services and endpoints. These can overlap. This method attempts to return a single list without duplicates.
/// </summary>
private static List<DisplayedEndpoint> GetEndpoints(ResourceViewModel resource)
{
return ResourceEndpointHelpers.GetEndpoints(resource, includeInteralUrls: false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,27 @@
@using Aspire.Dashboard.Resources
@inject IStringLocalizer<Columns> Loc

@if (Resource.IsProject() && Resource.TryGetProjectPath(out var projectPath))
@if (ContentAfterValue is not null)
{
// NOTE projects are also executables, so we have to check for projects first
<GridValue Value="@Path.GetFileName(projectPath)"
ValueToCopy="@projectPath"
EnableHighlighting="true"
HighlightText="@FilterText"
PreCopyToolTip="@Loc[nameof(Columns.SourceColumnSourceCopyFullPathToClipboard)]"
ToolTip="@projectPath" />
}
else if (Resource.TryGetExecutablePath(out var executablePath))
{
Resource.TryGetExecutableArguments(out var arguments);
var argumentsString = arguments.IsDefaultOrEmpty ? "" : string.Join(" ", arguments);
var fullCommandLine = $"{executablePath} {argumentsString}";

<GridValue Value="@Path.GetFileName(executablePath)"
ValueToCopy="@fullCommandLine"
<GridValue Value="@Value"
ValueToCopy="@ValueToCopy"
EnableHighlighting="true"
HighlightText="@FilterText"
PreCopyToolTip="@Loc[nameof(Columns.SourceColumnDisplayCopyCommandToClipboard)]"
ToolTip="@fullCommandLine">
ToolTip="@Tooltip">
<ContentAfterValue>
<span class="subtext">@argumentsString</span>
<span class="subtext">@ContentAfterValue</span>
</ContentAfterValue>
</GridValue>
}
else if (Resource.TryGetContainerImage(out var containerImage))
{
<GridValue Value="@containerImage"
EnableHighlighting="true"
HighlightText="@FilterText"
PreCopyToolTip="@Loc[nameof(Columns.SourceColumnSourceCopyContainerToClipboard)]"
ToolTip="@containerImage" />
}
else if (Resource.Properties.TryGetValue(KnownProperties.Resource.Source, out var value) && value.HasStringValue)
else
{
<GridValue Value="@value.StringValue"
<GridValue Value="@Value"
ValueToCopy="@ValueToCopy"
EnableHighlighting="true"
HighlightText="@FilterText"
PreCopyToolTip="@Loc[nameof(Columns.SourceColumnSourceCopyContainerToClipboard)]"
ToolTip="@value.StringValue" />
PreCopyToolTip="@Loc[nameof(Columns.SourceColumnSourceCopyFullPathToClipboard)]"
ToolTip="@Tooltip" />
}

@code {
Expand All @@ -53,4 +32,17 @@ else if (Resource.Properties.TryGetValue(KnownProperties.Resource.Source, out va

[Parameter, EditorRequired]
public required string FilterText { get; set; }

[Parameter, EditorRequired]
public required string Value { get; set; }

[Parameter]
public required string? ContentAfterValue { get; set; }

[Parameter, EditorRequired]
public required string ValueToCopy { get; set; }

[Parameter, EditorRequired]
public required string Tooltip { get; set; }

}
Loading