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

Create user screen #395

Merged
merged 38 commits into from
Jun 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
c72b7dd
feat: add condition that doesn't allow the user remove the admin role…
pkretzschmar May 12, 2022
328be65
feat: added selector for all admins and for all forwarders users
pkretzschmar May 12, 2022
b0728ae
fix: removed unused stuff and added new users table
pkretzschmar May 12, 2022
7e1b1c6
fix: removed unused files
pkretzschmar May 12, 2022
98e3f9c
feat: added new users table
pkretzschmar May 12, 2022
dd53b4f
feat: added new page that display info of specific user
pkretzschmar May 12, 2022
1d9f92d
Merge branch 'main' into feat/admin-screen
pkretzschmar May 12, 2022
ac99dbe
hotfix: eslint error
pkretzschmar May 12, 2022
1ece643
hotfix: change URL from pool to users
pkretzschmar May 19, 2022
5393847
hotfix: fix user check and removed the span tags
pkretzschmar May 19, 2022
fb6e83e
style: added opacity if role is not added
pkretzschmar May 19, 2022
f178a8c
hotfix: substitute roleBadge by InlineLabel component
pkretzschmar May 19, 2022
289a883
hotfix: space between arrows
pkretzschmar May 19, 2022
5f6537f
hotfix: removed table header component
pkretzschmar May 19, 2022
d4a4850
fix: remove select from search bar and fix the wrong behavior
pkretzschmar May 20, 2022
fe18f3d
fix: merge conflicts
pkretzschmar May 20, 2022
5864448
chore(frontend): delete no longer used components AddDialog & DeleteD…
mattyg May 23, 2022
544a2ba
feat(frontend): rename 'Quantifier Pool' nav item to 'Users'
mattyg May 23, 2022
559558b
refactor(frontend): call promise function with void to avoid needing …
mattyg May 23, 2022
989c33a
refactor(frontend): instead of returing empty JSX element when requir…
mattyg May 23, 2022
12139b4
refactor(frontend): remove additional dependency 'classnames' -- the …
mattyg May 23, 2022
d515781
refactor(frontend): remove non-null assertion within component render…
mattyg May 23, 2022
4638935
refactor(frontend): replace usage of getUsername with user.nameRealiz…
mattyg May 23, 2022
564b0b0
fix(frontend): ensure non-selected roles are more transparent, decrea…
mattyg May 23, 2022
1f0b8ca
refactor(frontend): move conditional outside of function parameters
mattyg May 23, 2022
42b5ac0
refactor(frontend): modify InlineLabel so it cannot break to a new li…
mattyg May 23, 2022
9037f4c
fix(frontend): ensure users are filtered by name lowercased
mattyg May 23, 2022
65542e6
wip: fix search on table
pkretzschmar Jun 1, 2022
999898c
fix: users table responsivity
Jun 1, 2022
882ae3e
fix: merge conflicts
Jun 1, 2022
3c5e373
fix: eslint errors
Jun 1, 2022
07e1c84
Fix: Uncomment text
kristoferlund Jun 3, 2022
6eb8bb7
Add: Constant for page length
kristoferlund Jun 3, 2022
ea0b36f
Fix: prevent word break in label
kristoferlund Jun 3, 2022
fae3dda
Fix: Paddings etc
kristoferlund Jun 3, 2022
f79e05b
CHANGELOG
kristoferlund Jun 3, 2022
60c076b
Fix: Reactive layout
kristoferlund Jun 3, 2022
103bec8
Fix: Paddings
kristoferlund Jun 3, 2022
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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Convert the Quantifier Pool screen to a User Admin screen #320 #395

### Fixed

## [0.7.0] - 2022-05-31
Expand Down
6 changes: 6 additions & 0 deletions packages/api/src/user/controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ const removeRole = async (

const { role } = req.body;
if (!role) throw new BadRequestError('Role is required');
if (role === UserRole.ADMIN) {
const allAdmins = await UserModel.find({ roles: { $in: ['ADMIN'] } });
if (allAdmins.length <= 1) {
throw new BadRequestError("You can't remove the last admin!");
}
}

const roleIndex = user.roles.indexOf(role);

Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/InlineLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const InlineLabel = ({
onClick={onClick}
className={classNames(
className,
'h-6 pl-1 pr-1 mr-1 text-xs text-white no-underline bg-gray-800 py-[1px] rounded',
'h-6 pl-1 pr-1 mr-1 whitespace-nowrap text-xs text-white no-underline bg-gray-800 py-[1px] rounded',
onClick ? 'cursor-pointer' : ''
)}
>
Expand Down
29 changes: 29 additions & 0 deletions packages/frontend/src/model/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ export const AllUsers = atom<UserDto[] | undefined>({
default: undefined,
});

export const AllAdminUsers = selector({
key: 'AllAdminUsers',
get: ({ get }) => {
const users = get(AllUsers);
if (users) {
return users.filter((user) => user.roles.includes(UserRole.ADMIN));
}
return undefined;
},
});

export const AllQuantifierUsers = selector({
key: 'AllQuantifierUsers',
get: ({ get }) => {
Expand All @@ -55,6 +66,17 @@ export const AllQuantifierUsers = selector({
},
});

export const AllForwarderUsers = selector({
key: 'AllForwarderUsers',
get: ({ get }) => {
const users = get(AllUsers);
if (users) {
return users.filter((user) => user.roles.includes(UserRole.FORWARDER));
}
return undefined;
},
});

export const useAllUsersQuery = (): AxiosResponse<unknown> => {
const allUsersQueryResponse = useAuthApiQuery(AllUsersQuery);
const [allUsers, setAllUsers] = useRecoilState(AllUsers);
Expand All @@ -72,6 +94,13 @@ export const useAllUsersQuery = (): AxiosResponse<unknown> => {
return allUsersQueryResponse;
};

/**
* Types for `useParams()`
*/
export type SingleUserParams = {
userId: string | undefined;
};

export const SingleUser = selectorFamily({
key: 'SingleUser',
get:
Expand Down
22 changes: 21 additions & 1 deletion packages/frontend/src/navigation/AuthenticatedRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { difference } from 'lodash';
import { useRecoilValue } from 'recoil';

const MyPraisePage = React.lazy(() => import('@/pages/MyPraise/MyPraisePage'));
const UserDetailsPage = React.lazy(
() => import('@/pages/UserDetails/UserDetailsPage')
);
const UsersPage = React.lazy(() => import('@/pages/Users/UsersPage'));

const PeriodsPage = React.lazy(() => import('@/pages/Periods/PeriodsPage'));
Expand Down Expand Up @@ -84,10 +87,27 @@ const AuthenticatedRoutes = (): JSX.Element | null => {
<MyPraisePage />
</Route>

<AuthRoute userRoles={userRoles} roles={[ROLE_ADMIN]} path={'/pool'}>
<AuthRoute
userRoles={userRoles}
roles={[ROLE_ADMIN]}
exact
path={'/users'}
>
<UsersPage />
</AuthRoute>

<AuthRoute
userRoles={userRoles}
roles={[ROLE_ADMIN]}
path={'/users/:userId'}
>
<UserDetailsPage />
</AuthRoute>

<Route exact path={'/periods'}>
pkretzschmar marked this conversation as resolved.
Show resolved Hide resolved
<PeriodsPage />
</Route>

<AuthRoute
userRoles={userRoles}
roles={[ROLE_ADMIN]}
Expand Down
6 changes: 1 addition & 5 deletions packages/frontend/src/navigation/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,7 @@ export default function Nav(): JSX.Element {
to="/mypraise"
/>
<AdminOnly>
<NavItem
icon={faUserFriends}
description="Quantifier pool"
to="/pool"
/>
<NavItem icon={faUserFriends} description="Users" to="/users" />
</AdminOnly>
<NavItem
icon={faCalculator}
Expand Down
95 changes: 95 additions & 0 deletions packages/frontend/src/pages/UserDetails/UserDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { shortenEthAddress } from 'api/dist/user/utils';
import { faUserGroup } from '@fortawesome/free-solid-svg-icons';
import BreadCrumb from '@/components/BreadCrumb';
import BackLink from '@/navigation/BackLink';
import { SingleUser, SingleUserParams, useAdminUsers } from '@/model/users';
import { useRecoilValue } from 'recoil';
import { useParams } from 'react-router-dom';
import { formatIsoDateUTC } from '@/utils/date';
import { classNames } from '@/utils/index';
import { UserDto, UserRole } from 'api/dist/user/types';
import { toast } from 'react-hot-toast';

const roles = [UserRole.ADMIN, UserRole.FORWARDER, UserRole.QUANTIFIER];

const UserDetailsPage = (): JSX.Element | null => {
const { userId } = useParams<SingleUserParams>();
const user = useRecoilValue(SingleUser(userId));
const { addRole, removeRole } = useAdminUsers();

const handleRole = async (role: UserRole, user: UserDto): Promise<void> => {
let resp;
const isRemove = user.roles.includes(role);
if (isRemove) {
resp = await removeRole(user._id, role);
} else {
resp = await addRole(user._id, role);
}
if (resp?.status === 200) {
toast.success(`Role ${isRemove ? 'removed' : 'added'} successfully!`);
}
};

if (!user) return null;

return (
<div className="max-w-2xl mx-auto">
<BreadCrumb name="User details" icon={faUserGroup} />
<BackLink to="/users" />
<div className="praise-box flex flex-col gap-2">
<span>User identity</span>
<span className="text-xl font-bold">
{user.ethereumAddress && shortenEthAddress(user.ethereumAddress)}
</span>
<div>
Created: {formatIsoDateUTC(user.createdAt)}
<br />
Last updated: {formatIsoDateUTC(user.updatedAt)}
</div>
</div>
<div className="praise-box flex flex-col gap-2">
<span>Linked Discord identity</span>
{user?.accounts?.map((account) => (
<>
<span className="text-xl font-bold">{account.name}</span>
<div>
Discord User ID: {account.user}
<br />
Created: {formatIsoDateUTC(account.createdAt)}
<br />
Last updated: {formatIsoDateUTC(account.updatedAt)}
</div>
</>
))}
</div>
<div className="praise-box">
<span className="text-xl font-bold">Roles</span>
<div className="flex gap-4 pt-5">
{roles.map((role) => (
<div
key={role}
className={classNames(
'flex gap-2 justify-center items-center py-2 px-3 rounded-md cursor-pointer bg-black',
user.roles.includes(role) ? '' : 'opacity-50'
)}
onClick={(): void => void handleRole(role, user)}
>
<input
checked={user.roles.includes(role)}
className="text-lime-500 cursor-pointer"
name={role}
type="checkbox"
readOnly
/>
<label className="text-white cursor-pointer" htmlFor={role}>
{role}
</label>
</div>
))}
</div>
</div>
</div>
);
};

export default UserDetailsPage;
52 changes: 4 additions & 48 deletions packages/frontend/src/pages/Users/UsersPage.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,14 @@
import BreadCrumb from '@/components/BreadCrumb';
import { useAdminUsers } from '@/model/users';
import PoolAddDialog from '@/pages/Users/components/AddDialog';
import { faPlus, faUserFriends } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Dialog } from '@headlessui/react';
import { UserRole } from 'api/dist/user/types';
import React from 'react';
import { faUserFriends } from '@fortawesome/free-solid-svg-icons';
import UsersStatistics from './components/UsersStatistics';
import UsersTable from './components/UsersTable';

const AddRoleButton = (): JSX.Element => {
const [isOpen, setIsOpen] = React.useState(false);
const { addRole } = useAdminUsers();

const handleAddQuantifierClick = (): void => {
setIsOpen(true);
};

const handleQuantifierAdded = (id: string): void => {
void addRole(id, UserRole.QUANTIFIER);
};

return (
<button className="praise-button" onClick={handleAddQuantifierClick}>
<FontAwesomeIcon icon={faPlus} size="1x" className="mr-2" />
Add quantifier
{isOpen ? (
<Dialog
open={isOpen}
onClose={(): void => setIsOpen(false)}
className="fixed inset-0 z-10 overflow-y-auto"
>
<div>
<PoolAddDialog
onClose={(): void => setIsOpen(false)}
onQuantifierAdded={handleQuantifierAdded}
/>
</div>
</Dialog>
) : null}
</button>
);
};

const UsersPage = (): JSX.Element => {
return (
<div className="max-w-2xl mx-auto">
<BreadCrumb name="Quantifier pool" icon={faUserFriends} />

<BreadCrumb name="Users" icon={faUserFriends} />
<UsersStatistics />
<div className="praise-box">
<div className="mb-2 text-right">
<React.Suspense fallback="Loading…">
<AddRoleButton />
</React.Suspense>
</div>
<UsersTable />
</div>
</div>
Expand Down
Loading