Skip to content
Closed
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
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/Resources.razor
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
</FluentTab>
</FluentTabs>
}
<div class="resources-grid-container" hidden="@(PageViewModel.SelectedViewKind == ResourceViewKind.Graph)">
<div @ref="_resourcesGridContainer" class="resources-grid-container" hidden="@(PageViewModel.SelectedViewKind == ResourceViewKind.Graph)">
<GridColumnManager @ref="_manager" Columns="@_gridColumns">
<FluentDataGrid @ref="_dataGrid"
ColumnResizeLabels="@_resizeLabels"
Expand Down
30 changes: 30 additions & 0 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,14 @@ public partial class Resources : ComponentBase, IComponentWithTelemetry, IAsyncD
private Task? _resourceSubscriptionTask;
private string? _elementIdBeforeDetailsViewOpened;
private FluentDataGrid<ResourceGridViewModel> _dataGrid = null!;
private ElementReference _resourcesGridContainer;
private GridColumnManager _manager = null!;
private int _maxHighlightedCount;
private readonly List<MenuButtonItem> _resourcesMenuItems = new();
private DotNetObjectReference<ResourcesInterop>? _resourcesInteropReference;
private IJSObjectReference? _jsModule;
private IJSObjectReference? _resourcesPageModule;
private IJSObjectReference? _resourcesGridKeyboardActivation;
private bool _graphInitialized;
private AspirePageContentLayout? _contentLayout;
private TotalItemsFooter _totalItemsFooter = default!;
Expand Down Expand Up @@ -393,6 +396,12 @@ private bool UpdateFromResource(ResourceViewModel resource, Func<string, bool> r

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_resourcesPageModule = await JS.InvokeAsync<IJSObjectReference>("import", "/Components/Pages/Resources.razor.js");
_resourcesGridKeyboardActivation = await _resourcesPageModule.InvokeAsync<IJSObjectReference>("initializeResourcesGridKeyboardActivation", _resourcesGridContainer);
}

// Check to see whether max item count should be set on every render.
// This is required because the data grid's virtualize component can be recreated on data change.
if (_dataGrid != null && FluentDataGridHelper<ResourceGridViewModel>.TrySetMaxItemCount(_dataGrid, 10_000))
Expand Down Expand Up @@ -990,11 +999,32 @@ public async ValueTask DisposeAsync()
_cts.Cancel();
_logsSubscription?.Dispose();
TelemetryContext.Dispose();
await DisposeResourcesGridKeyboardActivationAsync();
await JSInteropHelpers.SafeDisposeAsync(_resourcesPageModule);
await JSInteropHelpers.SafeDisposeAsync(_jsModule);

await TaskHelpers.WaitIgnoreCancelAsync(_resourceSubscriptionTask);
}

private async Task DisposeResourcesGridKeyboardActivationAsync()
{
if (_resourcesGridKeyboardActivation is null)
{
return;
}

try
{
await _resourcesGridKeyboardActivation.InvokeVoidAsync("dispose");
}
catch (JSDisconnectedException)
{
// This can happen when the browser has already disconnected during teardown.
}

await JSInteropHelpers.SafeDisposeAsync(_resourcesGridKeyboardActivation);
}

private async Task ContextMenuClosed(Microsoft.AspNetCore.Components.Web.MouseEventArgs args)
{
if (_contextMenu is { } menu)
Expand Down
113 changes: 113 additions & 0 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
const interactiveSelector = [
'a[href]',
'button',
'input',
'select',
'textarea',
'[role="button"]',
'[role="link"]',
'[role="menuitem"]',
'fluent-anchor',
'fluent-button',
'fluent-checkbox',
'fluent-combobox',
'fluent-menu-item',
'fluent-option',
'fluent-radio',
'fluent-search',
'fluent-select',
'fluent-switch',
'fluent-text-field'
].join(',');

export function shouldStopResourcesGridRowKeydown(event) {
return event.key === 'Enter';
}

export function initializeResourcesGridKeyboardActivation(grid) {
if (!grid) {
return {
dispose() {
}
};
}

const registeredElements = new Set();

const onKeyDown = event => {
// The data grid also treats Enter as row activation. Stop only the keys
// that activate focused controls so Tab, arrows, Escape, and shortcuts
// keep bubbling through the grid.
if (shouldStopResourcesGridRowKeydown(event)) {
event.stopPropagation();
}
};

const registerElement = element => {
if (!registeredElements.has(element)) {
element.addEventListener('keydown', onKeyDown);
registeredElements.add(element);
}
};

const unregisterElement = element => {
if (registeredElements.delete(element)) {
element.removeEventListener('keydown', onKeyDown);
}
};

const registerTree = node => {
if (node.nodeType !== Node.ELEMENT_NODE) {
return;
}

if (node.matches(interactiveSelector)) {
registerElement(node);
}

for (const element of node.querySelectorAll(interactiveSelector)) {
registerElement(element);
}
};

const unregisterTree = node => {
if (node.nodeType !== Node.ELEMENT_NODE) {
return;
}

if (node.matches(interactiveSelector)) {
unregisterElement(node);
}

for (const element of node.querySelectorAll(interactiveSelector)) {
unregisterElement(element);
}
};

registerTree(grid);

const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
registerTree(node);
}

for (const node of mutation.removedNodes) {
unregisterTree(node);
}
}
});
observer.observe(grid, { childList: true, subtree: true });

return {
dispose() {
observer.disconnect();

for (const element of registeredElements) {
element.removeEventListener('keydown', onKeyDown);
}

registeredElements.clear();
}
};
}
Loading
Loading