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

Refactor common code for logs pages #110

Merged
merged 1 commit into from
Oct 9, 2023
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
202 changes: 17 additions & 185 deletions src/Aspire.Dashboard/Components/Pages/ContainerLogs.razor
Original file line number Diff line number Diff line change
@@ -1,193 +1,25 @@
@page "/ContainerLogs/{containerid?}"
@page "/ContainerLogs/{containername?}"
@using Aspire.Dashboard.Model
@inject IDashboardViewModelService DashboardViewModelService
@inject IJSRuntime JS
@inject NavigationManager NavigationManager
@implements IAsyncDisposable
@inherits ResourceLogsBase<ContainerViewModel>

<PageTitle>Microsoft.Aspire Container Logs</PageTitle>

<h1>Container Logs</h1>

<div>
<FluentStack Orientation="Orientation.Vertical">
<FluentStack Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
<FluentSelect TOption="ContainerViewModel"
Items="@Containers"
OptionValue="@(c => c.ContainerId)"
OptionText="GetContainerDisplayText"
@bind-SelectedOption="_selectedContainer"
@bind-SelectedOption:after="HandleSelectedOptionChangedAsync" />
<FluentLabel Typo="Typography.Body">@_status</FluentLabel>
</FluentStack>
<LogViewer @ref="_logViewer" />
</FluentStack>
</div>

@code {
@{
base.BuildRenderTree(__builder);
smitpatel marked this conversation as resolved.
Show resolved Hide resolved
}

@code {
[Parameter]
public string? ContainerId { get; set; }

private ContainerViewModel? _selectedContainer;
private Dictionary<string, ContainerViewModel> _containerNameMapping = new();
private IEnumerable<ContainerViewModel> Containers => _containerNameMapping.Select(kvp => kvp.Value).OrderBy(c => c.Name);
private LogViewer? _logViewer;
private CancellationTokenSource _watchContainersTokenSource = new CancellationTokenSource();
private CancellationTokenSource? _watchLogsTokenSource;
private string _status = LogStatus.Initializing;

protected override async Task OnInitializedAsync()
{
_status = LogStatus.LoadingContainers;

var initialList = await DashboardViewModelService.GetContainersAsync();

foreach (var result in initialList)
{
_containerNameMapping[result.Name] = result;
}

if (ContainerId is not null)
{
_selectedContainer = initialList?.FirstOrDefault(c => string.Equals(ContainerId, c.ContainerId, StringComparison.Ordinal));
}
else if (initialList?.Count > 0)
{
_selectedContainer = initialList[0];
}

await LoadLogsAsync();

_ = Task.Run(async () =>
{
await foreach (var componentChanged in DashboardViewModelService.WatchContainersAsync(existingContainers: initialList?.Select(t => t.NamespacedName), cancellationToken: _watchContainersTokenSource.Token))
{
await OnContainerListChanged(componentChanged.ObjectChangeType, componentChanged.Component);
}
});
}

private Task ClearLogsAsync()
=> _logViewer is not null ? _logViewer.ClearLogsAsync() : Task.CompletedTask;

private async ValueTask LoadLogsAsync()
{
if (_selectedContainer is null)
{
_status = LogStatus.NoContainerSelected;
}
else if (_logViewer is null)
{
_status = LogStatus.InitializingLogViewer;
}
else
{
_watchLogsTokenSource = new CancellationTokenSource();
if (await _selectedContainer.LogSource.StartAsync(_watchLogsTokenSource.Token))
{
_ = Task.Run(async () =>
{
await _logViewer.WatchLogsAsync(() => _selectedContainer.LogSource.WatchOutputLogAsync(_watchLogsTokenSource.Token), LogEntryType.Default);
});

_ = Task.Run(async () =>
{
await _logViewer.WatchLogsAsync(() => _selectedContainer.LogSource.WatchErrorLogAsync(_watchLogsTokenSource.Token), LogEntryType.Error);
});

_status = LogStatus.WatchingLogs;
}
else
{
_watchLogsTokenSource = null;
_status = LogStatus.FailedToInitialize;
}
}
}

private async Task HandleSelectedOptionChangedAsync()
{
if (_selectedContainer is not null)
{
// Change the URL
NavigationManager.NavigateTo($"/containerLogs/{_selectedContainer.ContainerId}");
await StopWatchingLogsAsync();
await ClearLogsAsync();
await LoadLogsAsync();
}
}

private async Task OnContainerListChanged(ObjectChangeType changeType, ContainerViewModel containerViewModel)
{
if (changeType == ObjectChangeType.Added)
{
_containerNameMapping[containerViewModel.Name] = containerViewModel;

if (_selectedContainer is null)
{
if (string.IsNullOrEmpty(ContainerId) || string.Equals(ContainerId, containerViewModel.ContainerId, StringComparison.Ordinal))
{
_selectedContainer = containerViewModel;
await LoadLogsAsync();
}
}
}
else if (changeType == ObjectChangeType.Modified)
{
_containerNameMapping[containerViewModel.Name] = containerViewModel;
}
else if (changeType == ObjectChangeType.Deleted)
{
_containerNameMapping.Remove(containerViewModel.Name);
if (string.Equals(_selectedContainer?.Name, containerViewModel.Name, StringComparison.Ordinal))
{
if (_containerNameMapping.Count > 0)
{
_selectedContainer = Containers.First();
await HandleSelectedOptionChangedAsync();
}
}
}

await InvokeAsync(StateHasChanged);
}

private static string GetContainerDisplayText(ContainerViewModel container)
{
string stateText = "";
if (string.IsNullOrEmpty(container.State))
{
stateText = " (Unknown State)";
}
else if (container.State != "Running")
{
stateText = $" ({container.State})";
}
return $"{container.Name}{stateText}";
}
public string? ContainerName { get; set; }
smitpatel marked this conversation as resolved.
Show resolved Hide resolved

public async ValueTask DisposeAsync()
{
await DisposeWatchContainersTokenSource();
await StopWatchingLogsAsync();
}
protected override string? ResourceName => ContainerName;
protected override string ResourceType => "Container";
protected override string LoadingResourcesMessage => LogStatus.LoadingContainers;
protected override string NoResourceSelectedMessage => LogStatus.NoContainerSelected;
protected override string LogsNotAvailableMessage => LogStatus.FailedToInitialize;
protected override string UrlPrefix => "/ContainerLogs";

private async Task DisposeWatchContainersTokenSource()
{
await _watchContainersTokenSource.CancelAsync();
_watchContainersTokenSource.Dispose();
}
protected override Task<List<ContainerViewModel>> GetResources(IDashboardViewModelService dashboardViewModelService)
=> dashboardViewModelService.GetContainersAsync();

private async Task StopWatchingLogsAsync()
{
if (_watchLogsTokenSource is not null)
{
await _watchLogsTokenSource.CancelAsync();
_watchLogsTokenSource.Dispose();
// The token source only gets created if selected container is not null
await _selectedContainer!.LogSource.StopAsync();
_watchLogsTokenSource = null;
}
}
protected override IAsyncEnumerable<ComponentChanged<ContainerViewModel>> WatchResources(IDashboardViewModelService dashboardViewModelService, IEnumerable<NamespacedName>? initialList, CancellationToken cancellationToken)
=> dashboardViewModelService.WatchContainersAsync(initialList, cancellationToken);
}
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/Containers.razor
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
OnClick="async() => await ShowEnvironmentVariables(context)">View</FluentButton>
</TemplateColumn>
<TemplateColumn Title="Logs">
<FluentAnchor Appearance="Appearance.Lightweight" Href="@($"/containerLogs/{context.ContainerId}")">View</FluentAnchor>
<FluentAnchor Appearance="Appearance.Lightweight" Href="@($"/containerLogs/{context.Name}")">View</FluentAnchor>
</TemplateColumn>
</ChildContent>
<EmptyContent>
Expand Down
Loading