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

Improving Users admin page #1585

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
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
275 changes: 219 additions & 56 deletions Portal/src/Datahub.Portal/Pages/Tools/Users/UsersTable.razor
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@using Datahub.Core.Model.Projects
@using MudBlazor.Utilities
@using System.Text.Json.Nodes;
@using Datahub.Application.Services
Expand All @@ -10,52 +11,65 @@
@inject IUsersStatusService _userStatusService;
@inject DatahubPortalConfiguration _datahubPortalConfiguration
@inject IHttpClientFactory _httpClientFactory
@inject IJSRuntime _jsRuntime

<MudTable Striped Items="@UserWorkspaces" Filter="new Func<UserWorkspaces,bool>(FilterUserWorkspaces)">
<ToolBarContent>
<MudTextField @bind-Value="_searchString" Placeholder=@Localizer["Search by User or Workspace"] Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0" Label="@Localizer["Search"]"></MudTextField>
<MudTable Striped Items="@UserWorkspaces" ServerData="SortUserTable" @ref="userTable">
<ToolBarContent>
<MudTooltip Text=@Localizer["Copy the emails of all users in the current table (includes filtering)."]>
<DHButton Color="Color.Primary" OnClick="@CopyUsersToClipboard">
@_allButton
</DHButton>
</MudTooltip>
<MudTooltip Text=@Localizer["Download entire user table to CSV."]>
<DHButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@SidebarIcons.Download" OnClick="HandleDownload" Class="mr-4">
@Localizer["Download to CSV"]
</DHButton>
</MudTooltip>


<MudTextField
Value="_searchString" ValueChanged="SearchStringChanged" DebounceInterval="300" T="string" Immediate="true" Placeholder=@Localizer["Search by User or Workspace"]
Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-4" Label="@Localizer["Search"]">
</MudTextField>
<MudSpacer/>
<MudNumericField Value="_numberWorkspaces"
ValueChanged="NumberWorkspacesChanged"
DebounceInterval="300"
Label="Minimum number of workspaces"
T="int"
Immediate="true"
Min="0" />

<MudSpacer />
<MudSelect T="string" Value="_lastXDays" ValueChanged=LastXDaysChanged Variant="Variant.Text" Label="Time of last login" Dense="true">
<MudSelectItem T="string" Value="null" DataLabel="Lead(s)">@Localizer["All"]</MudSelectItem>
@foreach (var days in lastXDaysOptions)
{
<MudSelectItem T="string" Value="days">@Localizer[days]</MudSelectItem>
}
</MudSelect>
</ToolBarContent>
<HeaderContent>
<MudTh>
<MudTableSortLabel SortBy="new Func<UserWorkspaces, object>(x => x.User.DisplayName)" InitialDirection="SortDirection.Ascending">
<MudTableSortLabel SortLabel="User" SortBy="new Func<UserWorkspaces, object>(x => x.User.DisplayName)" InitialDirection="SortDirection.Ascending">
@Localizer["User"]
</MudTableSortLabel>
</MudTh>
@if (!HideInfo)
{
<MudTh>
<MudTableSortLabel SortBy="new Func<UserWorkspaces, object>(x => GetUserStatus(x.User.Email))">
@Localizer["Status"]
</MudTableSortLabel>
</MudTh>
}
<MudTh>
@Localizer["Workspaces"]
</MudTh>

<MudTh>
<MudTableSortLabel SortLabel="LastLoginDateTime" SortBy="new Func<UserWorkspaces, object>(x => x.User.LastLoginDateTime)">
@Localizer["Last login date"]
</MudTableSortLabel>
</MudTh>

</HeaderContent>
<RowTemplate>
<MudTd DataLabel="User">
<MudText Typo="Typo.h6">@context.User.DisplayName</MudText>
<MudText Typo="Typo.body2">@context.User.Email</MudText>
</MudTd>
@if (!HideInfo)
{
<MudTd DataLabel="Status">
@switch(GetUserStatus(@context.User.Email))
{
case "locked":
<DHIcon Icon="@Icons.Material.Filled.Lock" Title=@Localizer["Account locked"] />
break;
case "missing":
<DHIcon Icon="@Icons.Material.Filled.QuestionMark" Title=@Localizer["Account missing from MSGraph"] />
break;
case "":
break;
}
</MudTd>
}
<MudTd DataLabel="Workspaces">
<ul style="">
@foreach(var workspace in context.Workspaces)
Expand All @@ -68,6 +82,9 @@
}
</ul>
</MudTd>
<MudTd DataLabel="Workspaces">
<MudText>@context.User.LastLoginDateTime</MudText>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager RowsPerPageString="@Localizer["Rows per page:"]"/>
Expand All @@ -78,57 +95,203 @@
[Parameter]
public ICollection<UserWorkspaces> UserWorkspaces { get; set; }

public IEnumerable<UserWorkspaces> _filteredData { get; set; }

[Parameter]
public bool HideInfo { get; set; }

private string _searchString;
private IJSObjectReference _module;

private MudTable<UserWorkspaces> userTable;

private string _searchString = "", _lastXDays = "";
private int _numberWorkspaces = 0;

private string _allButton = "Copy emails";
private List<string> _filteredEmails;

private string AcronymStyle => new StyleBuilder()
.Build();

public List<string> LockedUsers = new ();

public List<string> AllUsers = new ();

public bool LoadStatus = false;

private List<string> lastXDaysOptions =
[
"Last 7 days",
"Last 30 days",
"Last 90 days",
"Last 120 days",
"Last 365 days",
"> 1 year"
];

protected override async Task OnInitializedAsync()
{
var resultDict = await _userStatusService.GetUsersStatus();
if (resultDict != null)
_module = await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "./Pages/Tools/Users/UsersTable.razor.js");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, the load icon in UsersPage.razor displays forever.

Suggested change
}
LoadStatus = true;
}


private async Task<TableData<UserWorkspaces>> SortUserTable(TableState state, CancellationToken cancellationToken)
{
var filteredData = UserWorkspaces.AsEnumerable();
_allButton = "Copy emails";

// Apply search filter
if (!string.IsNullOrEmpty(_searchString))
{
filteredData = filteredData.Where(check => ((check.User.DisplayName?.Contains(_searchString, StringComparison.OrdinalIgnoreCase) ?? false)
|| (check.User.Email?.Contains(_searchString, StringComparison.OrdinalIgnoreCase) ?? false)
|| check.Workspaces.Any(x => x.Project_Acronym_CD?.Contains(_searchString, StringComparison.OrdinalIgnoreCase) ?? false)
|| check.Workspaces.Any(x => x.ProjectName?.Contains(_searchString, StringComparison.OrdinalIgnoreCase) ?? false)));
}

// Apply # of workspaces filter
if (_numberWorkspaces > 0)
{
filteredData = filteredData.Where(check => check.Workspaces.Count() >= _numberWorkspaces);
}
Console.WriteLine(_numberWorkspaces);

// Apply last X days filter
if(_lastXDays != null)
{
if (_lastXDays.Contains("7 days"))
{
filteredData = filteredData.Where(check => check.User.LastLoginDateTime.HasValue && check.User.LastLoginDateTime.Value >= DateTime.UtcNow.AddDays(-7) && check.User.LastLoginDateTime.Value <= DateTime.UtcNow);
}
else if (_lastXDays.Contains("30 days"))
{
filteredData = filteredData.Where(check => check.User.LastLoginDateTime.HasValue && check.User.LastLoginDateTime.Value >= DateTime.UtcNow.AddDays(-30) && check.User.LastLoginDateTime.Value <= DateTime.UtcNow);
}
else if (_lastXDays.Contains("90 days"))
{
filteredData = filteredData.Where(check => check.User.LastLoginDateTime.HasValue && check.User.LastLoginDateTime.Value >= DateTime.UtcNow.AddDays(-90) && check.User.LastLoginDateTime.Value <= DateTime.UtcNow);
}
else if (_lastXDays.Contains("120 days"))
{
filteredData = filteredData.Where(check => check.User.LastLoginDateTime.HasValue && check.User.LastLoginDateTime.Value >= DateTime.UtcNow.AddDays(-120) && check.User.LastLoginDateTime.Value <= DateTime.UtcNow);
}
else if (_lastXDays.Contains("365 days"))
{
filteredData = filteredData.Where(check => check.User.LastLoginDateTime.HasValue && check.User.LastLoginDateTime.Value >= DateTime.UtcNow.AddDays(-365) && check.User.LastLoginDateTime.Value <= DateTime.UtcNow);
}
else if (_lastXDays.Contains("year"))
{
filteredData = filteredData.Where(check => check.User.LastLoginDateTime.HasValue && check.User.LastLoginDateTime.Value < DateTime.UtcNow.AddDays(-365) && check.User.LastLoginDateTime.Value <= DateTime.UtcNow);
}
}

// Apply sorting
if (state.SortDirection != SortDirection.None)
{
Func<UserWorkspaces, object> sortBy = state.SortLabel switch
{
nameof(Datahub.Portal.Pages.Tools.Users.UserWorkspaces.User) => x => x.User.DisplayName,
nameof(Datahub.Portal.Pages.Tools.Users.UserWorkspaces.User.LastLoginDateTime) => x => x.User.LastLoginDateTime,
_ => x => x.User.DisplayName
};

filteredData = filteredData.OrderByDirection(state.SortDirection, sortBy);
}

// Apply pagination
var totalItems = filteredData.Count();
var pagedData = filteredData
.Skip(state.Page * state.PageSize)
.Take(state.PageSize)
.ToList();

_filteredEmails = filteredData.Where(x => x.User.Email?.Contains("@") ?? false).Select(x => x.User.Email).ToList();
_filteredData = filteredData;

return await Task.FromResult(new TableData<UserWorkspaces> { TotalItems = totalItems, Items = pagedData });
}

private async Task SearchStringChanged(string search)
{
_searchString = search;
await ReloadWorkspaceHealthCheckTableAsync();
}

private async Task NumberWorkspacesChanged(int num)
{
Console.WriteLine(_numberWorkspaces);
_numberWorkspaces = num;
await ReloadWorkspaceHealthCheckTableAsync();
}

private async Task LastXDaysChanged(string days)
{
_lastXDays = days;
await ReloadWorkspaceHealthCheckTableAsync();
}

private async Task ReloadWorkspaceHealthCheckTableAsync()
{
if (userTable is not null)
{
AllUsers = resultDict["all"];
LockedUsers = resultDict["locked"];
LoadStatus = true;
await userTable.ReloadServerData();
}
}

private string GetUserStatus(string email)

// Copy emails to clipboard
private async Task CopyUsersToClipboard()
{
_allButton = await CopyToClipboard(_filteredEmails) ? Localizer["Copied!"] : Localizer["Failed to copy"];
}

private async Task<bool> CopyToClipboard(List<string> emailList)
{
var status = "";
if (LockedUsers.Contains(email) && !LockedUsers.Any())
var result = false;
if (emailList.Any())
{
status = "locked";
} else if (!AllUsers.Contains(email) && !AllUsers.Any())
var tempstr = String.Join(",\n", emailList);
await _jsRuntime.InvokeVoidAsync("navigator.clipboard.writeText", tempstr);
result = true;
}
return result;
}

// CSV download
private async Task HandleDownload()
{
var fileStream = GenerateCsvStream();
var fileName = $"UserInfo_{DateTime.Now.ToString("yyyy-MM-dd")}.csv";
using var streamReference = new DotNetStreamReference(stream: fileStream);
await _module.InvokeVoidAsync("downloadFileFromStream", fileName, streamReference);
}

private Stream GenerateCsvStream()
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.WriteLine("Name,Email,Workspaces,Last Login");
foreach (var cost in _filteredData)
{
status = "missing";
writer.WriteLine($"\"{cost.User.DisplayName}\",\"{cost.User.Email}\",\"{AllWorkspaces(cost.Workspaces)}\",{cost.User.LastLoginDateTime?.ToString("g", CultureInfo.CreateSpecificCulture("en-CA"))}");
}
return status;

writer.Flush();
stream.Position = 0;
return stream;
}

private bool FilterUserWorkspaces(UserWorkspaces userWorkspaces)
private string AllWorkspaces(List<Datahub_Project> workspaces)
{
if (string.IsNullOrWhiteSpace(_searchString))
return true;
if (userWorkspaces.User.DisplayName.Contains(_searchString, StringComparison.OrdinalIgnoreCase))
return true;
// Check that userWorkspaces.User.Email is not null
if (userWorkspaces.User.Email != null)
string allWorkspaces = "";
foreach(var space in workspaces)
{
if (userWorkspaces.User.Email.Contains(_searchString, StringComparison.OrdinalIgnoreCase))
return true;
if(space.Equals(workspaces.Last()))
{
allWorkspaces += space.ProjectName + "(" + space.Project_Acronym_CD + ")";
}
else
{
allWorkspaces += space.ProjectName + "(" + space.Project_Acronym_CD + "), ";
}
}
return userWorkspaces.Workspaces.Any(x => x.Project_Acronym_CD.Contains(_searchString, StringComparison.OrdinalIgnoreCase)) ||
userWorkspaces.Workspaces.Any(x => x.ProjectName.Contains(_searchString, StringComparison.OrdinalIgnoreCase));
return allWorkspaces;
}
}
11 changes: 11 additions & 0 deletions Portal/src/Datahub.Portal/Pages/Tools/Users/UsersTable.razor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export async function downloadFileFromStream(fileName, contentStreamReference) {
const arrayBuffer = await contentStreamReference.arrayBuffer();
const blob = new Blob([arrayBuffer]);
const url = URL.createObjectURL(blob);
const anchorElement = document.createElement('a');
anchorElement.href = url;
anchorElement.download = fileName ?? '';
anchorElement.click();
anchorElement.remove();
URL.revokeObjectURL(url);
}
Loading