Skip to content

Commit

Permalink
Clean up most of razor code to code-behind files (#164)
Browse files Browse the repository at this point in the history
I applied this for files where there was non-trivial logic

Resolves #78
  • Loading branch information
smitpatel authored Oct 10, 2023
1 parent cc90d47 commit 73e92dd
Show file tree
Hide file tree
Showing 17 changed files with 967 additions and 892 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,6 @@ dotnet_diagnostic.IDE0161.severity = silent
# IDE0005: Remove unused usings. Ignore for shared src files since imports for those depend on the projects in which they are included.
dotnet_diagnostic.IDE0005.severity = silent

[{*.razor.cs,src/Aspire.Dashboard/Components/Pages/**.cs}]
[{*.razor.cs,src/Aspire.Dashboard/Components/**.cs}]
# CA2007: Consider calling ConfigureAwait on the awaited task
dotnet_diagnostic.CA2007.severity = silent
98 changes: 0 additions & 98 deletions src/Aspire.Dashboard/Components/Controls/LogViewer.razor
Original file line number Diff line number Diff line change
Expand Up @@ -11,101 +11,3 @@

</div>
</div>

@code {
private static readonly AnsiParser s_ansiParser = new();

private ConcurrentQueue<IEnumerable<LogEntry>> _preRenderQueue = new();
private bool renderComplete = false;
private IJSObjectReference? _jsModule;

internal async Task ClearLogsAsync(CancellationToken cancellationToken = default)
{
if (_jsModule is not null)
{
await _jsModule.InvokeVoidAsync("clearLogs", cancellationToken);
}
}

private ValueTask WriteLogsToDomAsync(IEnumerable<LogEntry> logs)
=> _jsModule is null ? ValueTask.CompletedTask : _jsModule.InvokeVoidAsync("addLogEntries", logs);

internal async Task WatchLogsAsync(Func<IAsyncEnumerable<string[]>> watchMethod, LogEntryType logEntryType)
{
string? parentTimestamp = null;
Guid? parentId = null;
int lineIndex = 0;
AnsiParser.ParserState? residualState = null;

await foreach (var logs in watchMethod())
{
var logEntries = new List<LogEntry>(logs.Length);
foreach (var log in logs)
{
var logEntry = LogEntry.Create(log, logEntryType);

var conversionResult = s_ansiParser.ConvertToHtml(logEntry.Content, residualState);
logEntry.Content = conversionResult.ConvertedText;
residualState = conversionResult.ResidualState;

if (logEntry.IsFirstLine)
{
parentTimestamp = logEntry.Timestamp;
parentId = logEntry.Id;
lineIndex = 0;
}
else if (parentId.HasValue)
{
logEntry.ParentTimestamp = parentTimestamp;
logEntry.ParentId = parentId;
logEntry.LineIndex = ++lineIndex;
}
logEntries.Add(logEntry);
}

await WriteLogsAsync(logEntries);
}
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_jsModule ??= await JS.InvokeAsync<IJSObjectReference>("import", "/_content/Aspire.Dashboard/Components/Controls/LogViewer.razor.js");

while (_preRenderQueue.TryDequeue(out var logs))
{
await WriteLogsToDomAsync(logs);
}
renderComplete = true;
}
}

private async Task WriteLogsAsync(IEnumerable<LogEntry> logs)
{
if (renderComplete)
{
await WriteLogsToDomAsync(logs);
}
else
{
_preRenderQueue.Enqueue(logs);
}
}

public async ValueTask DisposeAsync()
{
try
{
if (_jsModule is not null)
{
await _jsModule.DisposeAsync();
}
}
catch (JSDisconnectedException)
{
// Per https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-7.0#javascript-interop-calls-without-a-circuit
// this is one of the calls that will fail if the circuit is disconnected, and we just need to catch the exception so it doesn't pollute the logs
}
}
}
107 changes: 107 additions & 0 deletions src/Aspire.Dashboard/Components/Controls/LogViewer.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// 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.ConsoleLogs;
using Aspire.Dashboard.Model;
using Microsoft.JSInterop;

namespace Aspire.Dashboard.Components;
public partial class LogViewer
{
private static readonly AnsiParser s_ansiParser = new();

private readonly ConcurrentQueue<IEnumerable<LogEntry>> _preRenderQueue = new();
private bool _renderComplete;
private IJSObjectReference? _jsModule;

internal async Task ClearLogsAsync(CancellationToken cancellationToken = default)
{
if (_jsModule is not null)
{
await _jsModule.InvokeVoidAsync("clearLogs", cancellationToken);
}
}

private ValueTask WriteLogsToDomAsync(IEnumerable<LogEntry> logs)
=> _jsModule is null ? ValueTask.CompletedTask : _jsModule.InvokeVoidAsync("addLogEntries", logs);

internal async Task WatchLogsAsync(Func<IAsyncEnumerable<string[]>> watchMethod, LogEntryType logEntryType)
{
string? parentTimestamp = null;
Guid? parentId = null;
int lineIndex = 0;
AnsiParser.ParserState? residualState = null;

await foreach (var logs in watchMethod())
{
var logEntries = new List<LogEntry>(logs.Length);
foreach (var log in logs)
{
var logEntry = LogEntry.Create(log, logEntryType);

var conversionResult = s_ansiParser.ConvertToHtml(logEntry.Content, residualState);
logEntry.Content = conversionResult.ConvertedText;
residualState = conversionResult.ResidualState;

if (logEntry.IsFirstLine)
{
parentTimestamp = logEntry.Timestamp;
parentId = logEntry.Id;
lineIndex = 0;
}
else if (parentId.HasValue)
{
logEntry.ParentTimestamp = parentTimestamp;
logEntry.ParentId = parentId;
logEntry.LineIndex = ++lineIndex;
}
logEntries.Add(logEntry);
}

await WriteLogsAsync(logEntries);
}
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_jsModule ??= await JS.InvokeAsync<IJSObjectReference>("import", "/_content/Aspire.Dashboard/Components/Controls/LogViewer.razor.js");

while (_preRenderQueue.TryDequeue(out var logs))
{
await WriteLogsToDomAsync(logs);
}
_renderComplete = true;
}
}

private async Task WriteLogsAsync(IEnumerable<LogEntry> logs)
{
if (_renderComplete)
{
await WriteLogsToDomAsync(logs);
}
else
{
_preRenderQueue.Enqueue(logs);
}
}

public async ValueTask DisposeAsync()
{
try
{
if (_jsModule is not null)
{
await _jsModule.DisposeAsync();
}
}
catch (JSDisconnectedException)
{
// Per https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-7.0#javascript-interop-calls-without-a-circuit
// this is one of the calls that will fail if the circuit is disconnected, and we just need to catch the exception so it doesn't pollute the logs
}
}
}
107 changes: 11 additions & 96 deletions src/Aspire.Dashboard/Components/Dialogs/EnvironmentVariables.razor
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
<FluentStack Orientation="Orientation.Vertical">
<FluentToolbar Orientation="Orientation.Horizontal">
<FluentButton Appearance="Appearance.Lightweight"
IconEnd="@(defaultMasked ? unmaskIcon : maskIcon)"
Title="@(defaultMasked ? "Show Values" : "Hide Values")"
IconEnd="@(_defaultMasked ? _unmaskIcon : _maskIcon)"
Title="@(_defaultMasked ? "Show Values" : "Hide Values")"
OnClick="ToggleMaskState"
slot="end" />
<FluentSearch Placeholder="Filter..."
Immediate="true"
Autofocus="true"
@bind-Value="filter"
@bind-Value="_filter"
@oninput="HandleFilter"
AfterBindValue="HandleClear"
slot="end" />
Expand All @@ -24,10 +24,10 @@
Style="width:100%"
GenerateHeader="GenerateHeaderOption.Sticky"
GridTemplateColumns="1fr 1fr">
<TemplateColumn Title="Name" Class="nameColumn" SortBy="@nameSort" Sortable="true" Tooltip="true" >
<TemplateColumn Title="Name" Class="nameColumn" SortBy="@_nameSort" Sortable="true" Tooltip="true" >
<FluentStack Orientation="Orientation.Horizontal">
<span class="cellText" title="@context.Name">
<FluentHighlighter HighlightedText="@filter"
<FluentHighlighter HighlightedText="@_filter"
Text="@context.Name" />
</span>
@{
Expand All @@ -38,10 +38,10 @@
IconEnd="@(new Icons.Regular.Size16.Copy())"
Class="defaultHidden"
@onclick="@(() => CopyTextToClipboardAsync(context.Name, @anchor))" />
<FluentTooltip Anchor="@anchor" Position="TooltipPosition.Top">@preCopyText</FluentTooltip>
<FluentTooltip Anchor="@anchor" Position="TooltipPosition.Top">@PreCopyText</FluentTooltip>
</FluentStack>
</TemplateColumn>
<TemplateColumn Title="Value" Class="valueColumn" SortBy="@valueSort" Sortable="true">
<TemplateColumn Title="Value" Class="valueColumn" SortBy="@_valueSort" Sortable="true">
<FluentStack Orientation="Orientation.Horizontal" Width="400px;">
@if (context.IsValueMasked)
{
Expand All @@ -52,12 +52,12 @@
else
{
<span class="cellText" title="@context.Value">
<FluentHighlighter HighlightedText="@filter"
<FluentHighlighter HighlightedText="@_filter"
Text="@context.Value" />
</span>
}
<FluentButton Appearance="Appearance.Lightweight"
IconEnd="@(context.IsValueMasked ? unmaskIcon : maskIcon)"
IconEnd="@(context.IsValueMasked ? _unmaskIcon : _maskIcon)"
Title="@(context.IsValueMasked ? "Show Value" : "Hide Value")"
OnClick="() => ToggleMaskState(context)" />
@{
Expand All @@ -68,96 +68,11 @@
IconEnd="@(new Icons.Regular.Size16.Copy())"
Class="defaultHidden"
@onclick="@(() => CopyTextToClipboardAsync(context.Value, @anchor))" />
<FluentTooltip Anchor="@anchor" Position="TooltipPosition.Top">@preCopyText</FluentTooltip>
<FluentTooltip Anchor="@anchor" Position="TooltipPosition.Top">@PreCopyText</FluentTooltip>
</FluentStack>
</TemplateColumn>
</FluentDataGrid>
</FluentStack>

</div>
</FluentDialogBody>

@code {

[Parameter]
public List<EnvironmentVariableViewModel>? Content { get; set; }

private IQueryable<EnvironmentVariableViewModel>? FilteredItems =>
Content?.Where(vm =>
vm.Name.Contains(filter, StringComparison.CurrentCultureIgnoreCase) ||
vm.Value?.Contains(filter, StringComparison.CurrentCultureIgnoreCase) == true
)?.AsQueryable();

private const string preCopyText = "Copy to clipboard";
private const string postCopyText = "Copied!";

private string filter = "";
private bool defaultMasked = true;

private Icon maskIcon = new Icons.Regular.Size16.EyeOff();
private Icon unmaskIcon = new Icons.Regular.Size16.Eye();

private GridSort<EnvironmentVariableViewModel> nameSort = GridSort<EnvironmentVariableViewModel>
.ByAscending(vm => vm.Name);
private GridSort<EnvironmentVariableViewModel> valueSort = GridSort<EnvironmentVariableViewModel>
.ByAscending(vm => vm.Value);

private void ToggleMaskState()
{
defaultMasked = !defaultMasked;
if (Content is not null)
{
foreach (var vm in Content)
{
vm.IsValueMasked = defaultMasked;
}
}
}

private void ToggleMaskState(EnvironmentVariableViewModel vm)
{
vm.IsValueMasked = !vm.IsValueMasked;
CheckAllMaskStates();
}

private void HandleFilter(ChangeEventArgs args)
{
if (args.Value is string newFilter)
{
filter = newFilter;
}
}

private void HandleClear(string? value)
{
filter = value ?? string.Empty;
}

private void CheckAllMaskStates()
{
if (Content is not null)
{
bool foundMasked = false;
bool foundUnmasked = false;
foreach (var vm in Content)
{
foundMasked |= vm.IsValueMasked;
foundUnmasked |= !vm.IsValueMasked;
}

if (!foundMasked && foundUnmasked)
{
defaultMasked = false;
}
else if (foundMasked && !foundUnmasked)
{
defaultMasked = true;
}
}
}

private async Task CopyTextToClipboardAsync(string? text, string id)
{
await JS.InvokeVoidAsync("copyTextToClipboard", id, text, preCopyText, postCopyText);
}
}
</FluentDialogBody>
Loading

0 comments on commit 73e92dd

Please sign in to comment.