Merge pull request 'devexpress' (#2) from devexpress into main

Reviewed-on: #2
This commit is contained in:
Holger Börchers 2020-07-27 21:24:28 +02:00
commit 114b52c963
17 changed files with 305 additions and 175 deletions

View File

@ -1,4 +1,4 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@ -11,7 +11,6 @@ namespace UserService.DatabaseLayer.DataModels
public class SecurityGroup : Member
{
}
public class User : Member
@ -23,8 +22,6 @@ namespace UserService.DatabaseLayer.DataModels
public string FullName => $"{FirstName} {LastName}";
public IEnumerable<UserMember> MemberOf { get; set; } = new List<UserMember>();
public User Clone() => (User)MemberwiseClone();
}
public class UserMember
@ -45,7 +42,7 @@ namespace UserService.DatabaseLayer.DataModels
public ICollection<UserMember> Members { get; set; } = new List<UserMember>();
}
public abstract class Node
public abstract class Node : ICloneable
{
public int Id { get; set; }
[Required] public string CommonName { get; set; } = null!;
@ -58,5 +55,8 @@ namespace UserService.DatabaseLayer.DataModels
{
return $"[{GetType().Name}] {Id:D5} {CommonName}";
}
/// <inheritdoc />
public virtual object Clone() => MemberwiseClone();
}
}

View File

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
@ -8,7 +10,7 @@ using UserService.DatabaseLayer.DataModels;
namespace UserService.DatabaseLayer.Repository
{
public class BaseRepository<T> where T : class
public class BaseRepository<T> where T : Node
{
private readonly Func<UserServiceDbContext, DbSet<T>> _context;
@ -20,7 +22,7 @@ namespace UserService.DatabaseLayer.Repository
public async Task<IReadOnlyList<T>> GetAllAsync(CancellationToken token = default)
{
await using var db = new UserServiceDbContext();
return await _context(db).ToListAsync(token);
return await _context(db).Include(x => x.Parent).ToListAsync(token);
}
public async Task<T?> GetAsync(Expression<Func<T, bool>> predicate, CancellationToken token = default)

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Threading;
@ -33,15 +32,8 @@ namespace UserService.DatabaseLayer.Repository
public class NodesRepository : INodesRepository
{
private readonly IUsersRepository _users;
private readonly ISecurityGroupsRepository _securityGroups;
private readonly IOrganizationUnitsRepository _organizationUnits;
public NodesRepository(IUsersRepository users, ISecurityGroupsRepository securityGroups, IOrganizationUnitsRepository organizationUnits)
public NodesRepository()
{
_users = users;
_securityGroups = securityGroups;
_organizationUnits = organizationUnits;
}
public static async IAsyncEnumerable<Node> GetNodesAsync([EnumeratorCancellation] CancellationToken token = default)

View File

@ -0,0 +1,27 @@
//------------------------------------------------------------------------------
// Generated by the DevExpress.Blazor package.
// To prevent this operation, add the DxExtendStartupHost property to the project and set this property to False.
//
// UserService.Test.csproj:
//
// <Project Sdk="Microsoft.NET.Sdk.Web">
// <PropertyGroup>
// <TargetFramework>netcoreapp3.1</TargetFramework>
// <DxExtendStartupHost>False</DxExtendStartupHost>
// </PropertyGroup>
//------------------------------------------------------------------------------
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
[assembly: HostingStartup(typeof(UserService.Test.DevExpressHostingStartup))]
namespace UserService.Test {
public partial class DevExpressHostingStartup : IHostingStartup {
void IHostingStartup.Configure(IWebHostBuilder builder) {
builder.ConfigureServices((serviceCollection) => {
serviceCollection.AddDevExpressBlazor();
});
}
}
}

Binary file not shown.

View File

@ -1,9 +1,9 @@
@page "/counter"
@page "/directory"
@using UserService.DatabaseLayer.DataModels
@using UserService.DatabaseLayer.Repository
@inject IOrganizationUnitsRepository OuRepository
<h1>Tree</h1>
<h1>TODO</h1>
@if (_organizationUnits == null)
{
@ -13,18 +13,17 @@
}
else
{
<MatNavMenu>
@foreach (var unit in _organizationUnits)
{
<OrgUnitItem OrganizationUnit="@unit"/>
}
</MatNavMenu>
}
@code {
private IReadOnlyList<OrganizationUnit> _organizationUnits;
private OrganizationUnit _selectedOu;
protected override async Task OnInitializedAsync()
{
_organizationUnits = await OuRepository.GetAllAsync().ConfigureAwait(false);
_organizationUnits = (await OuRepository.GetAllAsync().ConfigureAwait(false)).Where(x=> x.Parent is null).ToList();
}
}

View File

@ -1,105 +1,8 @@
@page "/"
@using UserService.DatabaseLayer.DataModels
@using UserService.DatabaseLayer.Repository
@inject IUsersRepository UsersRepository
<h1>User service</h1>
@if (_users == null)
{
<p>
<em>Loading...</em>
</p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Common Name</th>
<th>Full name</th>
<th>Description</th>
<th>E-Mail</th>
<th>Is active</th>
<th> </th>
<th> </th>
</tr>
</thead>
<tbody>
@foreach (var user in _users)
{
<tr>
<td>@user.CommonName</td>
<td>@user.FullName</td>
<td>@user.Description</td>
<td>@user.EMail</td>
<td>@user.IsActive</td>
<td><a href="" @onclick="@(e => EditUser(user))">edit</a></td>
<td><a href="" @onclick="@(e => DeleteUser(user))">delete</a></td>
</tr>
}
</tbody>
</table>
<MatButton @onclick="@(e => EditUser(new User()))">Create user</MatButton>
}
<MatDialog @bind-IsOpen="@_dialogIsOpen">
@if (_userToEdit != null)
{
<MatDialogTitle>@(_userToEdit.Id == 0 ? "New" : "Edit") @_userToEdit.CommonName (@_userToEdit.Id)</MatDialogTitle>
<MatDialogContent>
<MatTextField Label="Common name" @bind-Value="@_userToEdit.CommonName" ReadOnly="@(_userToEdit.Id != 0)"></MatTextField>
<p />
<MatTextField Label="First name" @bind-Value="@_userToEdit.FirstName"></MatTextField>
<MatTextField Label="Last name" @bind-Value="@_userToEdit.LastName"></MatTextField>
<p />
<MatTextField Label="Description" @bind-Value="@_userToEdit.Description"></MatTextField>
<p />
<MatTextField Label="E-Mail" @bind-Value="@_userToEdit.EMail"></MatTextField>
<MatCheckbox Label="Is active" @bind-Value="@_userToEdit.IsActive"></MatCheckbox>
</MatDialogContent>
}
else
{
<MatDialogTitle>No securityGroup selected</MatDialogTitle>
}
<MatDialogActions>
<MatButton OnClick="@(e => { _dialogIsOpen = false; })">No Thanks</MatButton>
<MatButton OnClick="@OkClick">OK</MatButton>
</MatDialogActions>
</MatDialog>
@code {
bool _dialogIsOpen;
User? _userToEdit;
private IReadOnlyList<User> _users;
protected override async Task OnInitializedAsync()
{
_users = await UsersRepository.GetAllAsync();
}
private void EditUser(User user)
{
_dialogIsOpen = true;
_userToEdit = user.Clone();
}
async Task OkClick()
{
await UsersRepository.UpdateAsync(_userToEdit).ConfigureAwait(false);
await OnInitializedAsync().ConfigureAwait(false);
_dialogIsOpen = false;
}
private async Task DeleteUser(User user)
{
await UsersRepository.DeleteAsync(user).ConfigureAwait(false);
await OnInitializedAsync().ConfigureAwait(false);
}
}

View File

@ -0,0 +1,95 @@
@page "/securitygroups"
@using UserService.DatabaseLayer.DataModels
@using UserService.DatabaseLayer.Repository
@inject ISecurityGroupsRepository SecurityGroupsRepository
@inject IOrganizationUnitsRepository OrganizationUnits
<h1>Table of all security groups</h1>
@if (_securityGroups == null)
{
<p>
<em>Loading...</em>
</p>
}
else
{
<MatTable Items="@_securityGroups" class="mat-elevation-z5">
<MatTableHeader>
<th style="width: 30%">Common Name</th>
<th style="width: 20%">Description</th>
<th style="width: 20%">E-Mail</th>
<th style="width: 20%">Parent</th>
<th style="width: auto"> </th>
<th style="width: auto"> </th>
</MatTableHeader>
<MatTableRow>
<td>@context.CommonName</td>
<td>@context.Description</td>
<td>@context.EMail</td>
<td>@context.Parent</td>
<td><a href="securitygroups" @onclick="@(e => EditSecurityGroup(context))">edit</a></td>
<td><a href="securitygroups" @onclick="@(e => DeleteSecurityGroup(context))">delete</a></td>
</MatTableRow>
</MatTable>
<MatButton @onclick="@(e => EditSecurityGroup(new SecurityGroup()))">Create new group</MatButton>
}
<MatDialog @bind-IsOpen="@_dialogIsOpen">
@if (_securityGroupToEdit != null)
{
<MatDialogTitle>@(_securityGroupToEdit.Id == 0 ? "New" : "Edit") @_securityGroupToEdit.CommonName (@_securityGroupToEdit.Id)</MatDialogTitle>
<MatDialogContent>
<MatTextField Label="Common name" @bind-Value="@_securityGroupToEdit.CommonName" ReadOnly="@(_securityGroupToEdit.Id != 0)"></MatTextField>
<p />
<MatTextField Label="Description" @bind-Value="@_securityGroupToEdit.Description"></MatTextField>
<MatTextField Label="E-Mail" @bind-Value="@_securityGroupToEdit.EMail"></MatTextField>
<p />
<MatAutocompleteList Items="@_organizationUnits" TItem="Node" Label="Parent" CustomStringSelector="@(i => i.CommonName)" @bind-Value="@_securityGroupToEdit.Parent"></MatAutocompleteList>
</MatDialogContent>
}
else
{
<MatDialogTitle>No securityGroup selected</MatDialogTitle>
}
<MatDialogActions>
<MatButton OnClick="@(e => { _dialogIsOpen = false; })">No Thanks</MatButton>
<MatButton OnClick="@OkClick">OK</MatButton>
</MatDialogActions>
</MatDialog>
@code {
bool _dialogIsOpen;
SecurityGroup _securityGroupToEdit;
private IReadOnlyList<SecurityGroup> _securityGroups;
private IReadOnlyList<OrganizationUnit> _organizationUnits;
protected override async Task OnInitializedAsync()
{
_securityGroups = await SecurityGroupsRepository.GetAllAsync();
_organizationUnits = await OrganizationUnits.GetAllAsync().ConfigureAwait(false);
}
private void EditSecurityGroup(SecurityGroup securityGroup)
{
_dialogIsOpen = true;
_securityGroupToEdit = (SecurityGroup)securityGroup.Clone();
}
async Task OkClick()
{
await SecurityGroupsRepository.UpdateAsync(_securityGroupToEdit).ConfigureAwait(false);
await OnInitializedAsync().ConfigureAwait(false);
_dialogIsOpen = false;
}
private async Task DeleteSecurityGroup(SecurityGroup securityGroup)
{
await SecurityGroupsRepository.DeleteAsync(securityGroup).ConfigureAwait(false);
await OnInitializedAsync().ConfigureAwait(false);
}
}

View File

@ -0,0 +1,66 @@
@page "/users"
@using UserService.DatabaseLayer.DataModels
@inherits UsersBase
<h1>List of all users</h1>
@if (Users == null)
{
<p>
<em>Loading...</em>
</p>
}
else
{
<MatTable Items="@Users" class="mat-elevation-z5">
<MatTableHeader>
<th style="width: 20%">Common Name</th>
<th style="width: 30%">Full name</th>
<th style="width: 20%">Description</th>
<th style="width: 20%">E-Mail</th>
<th style="width: 20%">Parent</th>
<th style="width: auto">Is active</th>
<th style="width: auto"> </th>
<th style="width: auto"> </th>
</MatTableHeader>
<MatTableRow>
<td>@context.CommonName</td>
<td>@context.FullName</td>
<td>@context.Description</td>
<td>@context.EMail</td>
<td>@context.Parent</td>
<td>@context.IsActive</td>
<td><a href="users" @onclick="@(e => EditUser(context))">edit</a></td>
<td><a href="users" @onclick="@(e => DeleteUser(context))">delete</a></td>
</MatTableRow>
</MatTable>
<MatButton @onclick="@(e => EditUser(new User()))">Create user</MatButton>
}
<MatDialog @bind-IsOpen="@DialogIsOpen">
@if (UserToEdit != null)
{
<MatDialogTitle>@(UserToEdit.Id == 0 ? "New" : "Edit") @UserToEdit.CommonName (@UserToEdit.Id)</MatDialogTitle>
<MatDialogContent>
<MatTextField Label="Common name" @bind-Value="@UserToEdit.CommonName" ReadOnly="@(UserToEdit.Id != 0)"></MatTextField>
<MatSlideToggle Label="Is active" @bind-Value="@UserToEdit.IsActive"></MatSlideToggle>
<p />
<MatTextField Label="First name" @bind-Value="@UserToEdit.FirstName"></MatTextField>
<MatTextField Label="Last name" @bind-Value="@UserToEdit.LastName"></MatTextField>
<p />
<MatTextField Label="Description" @bind-Value="@UserToEdit.Description"></MatTextField>
<MatTextField Label="E-Mail" @bind-Value="@UserToEdit.EMail"></MatTextField>
<p />
<MatAutocompleteList Items="@OrganizationUnits" TItem="Node" Label="Parent" CustomStringSelector="@(i => i.CommonName)" @bind-Value="@UserToEdit.Parent"></MatAutocompleteList>
</MatDialogContent>
}
else
{
<MatDialogTitle>No securityGroup selected</MatDialogTitle>
}
<MatDialogActions>
<MatButton OnClick="@(e => { DialogIsOpen = false; })">No Thanks</MatButton>
<MatButton OnClick="@OkClick">OK</MatButton>
</MatDialogActions>
</MatDialog>

View File

@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using UserService.DatabaseLayer.DataModels;
using UserService.DatabaseLayer.Repository;
namespace UserService.Pages
{
public class UsersBase : ComponentBase
{
[Inject] private IUsersRepository UsersRepository { get; set; } = null!;
[Inject] private IOrganizationUnitsRepository OrganizationUnitsRepository { get; set; } = null!;
protected bool DialogIsOpen { get; set; }
protected User? UserToEdit { get; private set; }
protected IReadOnlyList<User>? Users { get; private set; }
protected IReadOnlyList<OrganizationUnit>? OrganizationUnits { get; private set; }
protected override async Task OnInitializedAsync()
{
Users = await UsersRepository.GetAllAsync();
OrganizationUnits = await OrganizationUnitsRepository.GetAllAsync().ConfigureAwait(false);
}
protected void EditUser(User user)
{
DialogIsOpen = true;
UserToEdit = user;
}
protected async Task OkClick()
{
if (UserToEdit is null) return;
await UsersRepository.UpdateAsync(UserToEdit).ConfigureAwait(false);
await OnInitializedAsync().ConfigureAwait(false);
DialogIsOpen = false;
}
protected async Task DeleteUser(User user)
{
await UsersRepository.DeleteAsync(user).ConfigureAwait(false);
await OnInitializedAsync().ConfigureAwait(false);
}
}
}

View File

@ -8,31 +8,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>UserService</title>
<base href="~/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link href="css/site.css" rel="stylesheet" />
<base href="~/"/>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
<link href="css/site.css" rel="stylesheet"/>
<script src="_content/MatBlazor/dist/matBlazor.js"></script>
<link href="_content/MatBlazor/dist/matBlazor.css" rel="stylesheet" />
<link href="_content/MatBlazor/dist/matBlazor.css" rel="stylesheet"/>
</head>
<body>
<app>
<component type="typeof(App)" render-mode="ServerPrerendered" />
</app>
<app>
<component type="typeof(App)" render-mode="ServerPrerendered"/>
</app>
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.server.js"></script>
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
<script src="_framework/blazor.server.js"></script>
</body>
</html>

View File

@ -1,13 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace UserService
{

View File

@ -5,6 +5,7 @@
</button>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<ul class="nav flex-column">
<li class="nav-item px-3">
@ -13,20 +14,30 @@
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
<NavLink class="nav-link" href="users">
<span class="oi oi-plus" aria-hidden="true"></span> Users
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="securitygroups">
<span class="oi oi-plus" aria-hidden="true"></span> Security groups
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="directory">
<span class="oi oi-plus" aria-hidden="true"></span> Directory
</NavLink>
</li>
</ul>
</div>
@code {
private bool collapseNavMenu = true;
private bool _collapseNavMenu = true;
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private string NavMenuCssClass => _collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
_collapseNavMenu = !_collapseNavMenu;
}
}

View File

@ -1,15 +0,0 @@
@using UserService.DatabaseLayer.DataModels
<MatNavSubMenu>
<MatNavSubMenuHeader>
<MatNavItem AllowSelection="false"><MatIcon Icon="folder"></MatIcon>&nbsp; @OrganizationUnit.CommonName</MatNavItem>
</MatNavSubMenuHeader>
<MatNavSubMenuList>
<MatNavItem Disabled="true" Href="#">Item 6.A</MatNavItem>
<MatNavItem>Item 6.B</MatNavItem>
<MatNavItem>Item 6.C</MatNavItem>
</MatNavSubMenuList>
</MatNavSubMenu>
@code {
[Parameter]
public OrganizationUnit OrganizationUnit { get; set; }
}

View File

@ -1,3 +1,4 @@
using System.Net.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
@ -25,7 +26,7 @@ namespace UserService
services.AddSingleton<IUsersRepository, UsersRepository>();
services.AddSingleton<ISecurityGroupsRepository, SecurityGroupsRepository>();
services.AddSingleton<IOrganizationUnitsRepository, OrganizationUnitsRepository>();
//services.AddSingleton<INodesRepository, NodesRepository>();
services.AddScoped<HttpClient>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>

9
nuget.config Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
<add key="devexpress" value="https://nuget.devexpress.com/3zXs42uzzGPeHmfH1Bl7ERGp2eJ2t3ppmS2wZumhP3vJedKtPB/api" />
</packageSources>
</configuration>