Skip to content
This repository has been archived by the owner on Oct 23, 2021. It is now read-only.

Commit

Permalink
CA-863 - search users in admin UI (#423)
Browse files Browse the repository at this point in the history
  • Loading branch information
timstokman authored Jul 12, 2020
1 parent 565575b commit 2cbf141
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 12 deletions.
29 changes: 25 additions & 4 deletions CollAction/GraphQl/Queries/QueryGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using CollAction.Helpers;
using CollAction.Models;
using CollAction.Services.Crowdactions;
using CollAction.Services.User;
using GraphQL.Authorization;
using GraphQL.EntityFramework;
using GraphQL.Types;
Expand Down Expand Up @@ -107,9 +108,20 @@ public QueryGraph(IEfGraphQLService<ApplicationDbContext> entityFrameworkGraphQl
});

AddQueryField(
nameof(ApplicationDbContext.Users),
c => c.DbContext.Users,
typeof(ApplicationUserGraph)).AuthorizeWith(AuthorizationConstants.GraphQlAdminPolicy);
name: nameof(ApplicationDbContext.Users),
arguments: new QueryArgument[]
{
new QueryArgument<StringGraphType>() { Name = "search" }
},
resolve: c =>
{
string? search = c.GetArgument<string?>("search");
return c.GetUserContext()
.ServiceProvider
.GetRequiredService<IUserService>()
.SearchUsers(search);
},
graphType: typeof(ApplicationUserGraph)).AuthorizeWith(AuthorizationConstants.GraphQlAdminPolicy);

AddSingleField(
name: "user",
Expand All @@ -118,7 +130,16 @@ public QueryGraph(IEfGraphQLService<ApplicationDbContext> entityFrameworkGraphQl

FieldAsync<NonNullGraphType<IntGraphType>, int>(
"userCount",
resolve: c => c.GetUserContext().Context.Users.CountAsync()).AuthorizeWith(AuthorizationConstants.GraphQlAdminPolicy);
arguments: new QueryArguments(new QueryArgument<StringGraphType>() { Name = "search" }),
resolve: c =>
{
string? search = c.GetArgument<string?>("search");
return c.GetUserContext()
.ServiceProvider
.GetRequiredService<IUserService>()
.SearchUsers(search)
.CountAsync();
}).AuthorizeWith(AuthorizationConstants.GraphQlAdminPolicy);

AddSingleField(
name: "currentUser",
Expand Down
6 changes: 5 additions & 1 deletion CollAction/Services/User/IUserService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using CollAction.Services.User.Models;
using CollAction.Models;
using CollAction.Services.User.Models;
using Microsoft.AspNetCore.Identity;
using Newtonsoft.Json.Linq;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -26,5 +28,7 @@ public interface IUserService
Task<UserResult> FinishRegistration(NewUser newUser, string code);

Task<int> IngestUserEvent(ClaimsPrincipal trackedUser, JObject eventData, bool canTrack, CancellationToken token);

IQueryable<ApplicationUser> SearchUsers(string? searchString);
}
}
17 changes: 17 additions & 0 deletions CollAction/Services/User/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,23 @@ public async Task<UserResult> CreateUser(NewUser newUser)
return (IdentityResult.Success, code);
}

public IQueryable<ApplicationUser> SearchUsers(string? searchString)
{
if (!string.IsNullOrWhiteSpace(searchString))
{
#pragma warning disable CA1307 // Not needed, translated to sql
return context.Users
.Where(u => u.Email.Contains(searchString) ||
u.FirstName!.Contains(searchString) ||
u.LastName!.Contains(searchString));
#pragma warning restore CA1307 // Not needed, translated to sql
}
else
{
return context.Users;
}
}

public async Task<IdentityResult> ResetPassword(string email, string code, string password)
{
ApplicationUser? user = await userManager.FindByEmailAsync(email).ConfigureAwait(false);
Expand Down
37 changes: 30 additions & 7 deletions Frontend/src/components/Admin/Users/AdminListUsers.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { Paper, TableContainer, Table, TableHead, TableCell, TableRow, TableBody, TablePagination, Dialog, DialogTitle, DialogContent, DialogActions } from "@material-ui/core";
import { Paper, TableContainer, Table, TableHead, TableCell, TableRow, TableBody, TablePagination, Dialog, DialogTitle, DialogContent, DialogActions, TextField } from "@material-ui/core";
import { gql, useQuery, useMutation } from "@apollo/client";
import { IUser } from "../../../api/types";
import { Alert } from "../../Alert/Alert";
Expand All @@ -14,14 +14,16 @@ export default () => {
const [toDelete, setToDelete] = useState<IUser | null>(null);
const [info, setInfo] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState("");
const {data, loading, error: loadingError} = useQuery(
GET_USERS,
{
fetchPolicy: "cache-and-network", // To ensure it updates after deleting/editting
variables: {
skip: rowsPerPage * page,
take: rowsPerPage,
orderBy: "lastName"
orderBy: "lastName",
search: search
}
}
);
Expand Down Expand Up @@ -60,11 +62,27 @@ export default () => {
);
const userCount = data?.userCount ?? 0;

const onSearchChange = (ev: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setSearch(ev.target.value);
setPage(0);
};

const onDeleteUserClick = (user: IUser) => {
setDeleteDialogOpen(true);
setToDelete(user);
};

const onChangeRowsPerPage = (ev: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setPage(0);
setRowsPerPage(parseInt((ev.target.value)));
};

return <>
{ loading ? <Loader /> : null }
<Alert type="info" text={info} />
<Alert type="error" text={error} />
<Alert type="error" text={loadingError?.message} />
{ data?.users && data.users.length === 0 && search.length > 0 && <Alert type="error" text="No search results" /> }
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
<DialogTitle>Delete user { toDelete?.email }?</DialogTitle>
<DialogContent>
Expand All @@ -75,6 +93,7 @@ export default () => {
<Button onClick={() => setDeleteDialogOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<TextField label="Search user (case-sensitive)" fullWidth type="text" value={search} onChange={onSearchChange} />
<TableContainer component={Paper}>
<Table aria-label="simple table">
<TableHead>
Expand All @@ -97,11 +116,11 @@ export default () => {
<TableCell align="right">{ u.isAdmin ? "Yes" : "No" }</TableCell>
<TableCell align="right">{ Formatter.date(new Date(u.registrationDate)) } { Formatter.time(new Date(u.registrationDate)) }</TableCell>
<TableCell align="right"><Button to={`/admin/users/edit/${u.id}`}>Edit</Button></TableCell>
<TableCell align="right"><Button onClick={() => { setDeleteDialogOpen(true); setToDelete(u); }}>Delete</Button></TableCell>
<TableCell align="right"><Button onClick={() => onDeleteUserClick(u)}>Delete</Button></TableCell>
</TableRow>))
}
<TableRow>
<TablePagination count={userCount} page={page} rowsPerPageOptions={[5, 10, 25, 50]} rowsPerPage={rowsPerPage} onChangePage={(_ev, newPage) => setPage(newPage)} onChangeRowsPerPage={(ev) => { setPage(0); setRowsPerPage(parseInt((ev.target.value))) } } />
<TablePagination count={userCount} page={page} rowsPerPageOptions={[5, 10, 25, 50]} rowsPerPage={rowsPerPage} onChangePage={(_ev, newPage) => setPage(newPage)} onChangeRowsPerPage={onChangeRowsPerPage} />
</TableRow>
</TableBody>
</Table>
Expand All @@ -110,8 +129,12 @@ export default () => {
};

const GET_USERS = gql`
query GetUserData($skip: Int!, $take: Int!, $orderBy: String!) {
users(orderBy: [{ path: $orderBy, descending: false}], skip: $skip, take: $take) {
query GetUserData($skip: Int!, $take: Int!, $orderBy: String!, $search: String!) {
users(
orderBy: [{ path: $orderBy, descending: false}],
skip: $skip,
take: $take,
search: $search) {
id
email
isSubscribedNewsletter
Expand All @@ -122,7 +145,7 @@ const GET_USERS = gql`
registrationDate
representsNumberParticipants
}
userCount
userCount(search: $search)
}
`;

Expand Down

0 comments on commit 2cbf141

Please sign in to comment.