Skip to content

Commit

Permalink
User Management Skeleton
Browse files Browse the repository at this point in the history
  • Loading branch information
unmade committed Jun 7, 2021
1 parent d65df84 commit 3df0926
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 6 deletions.
49 changes: 49 additions & 0 deletions src/components/AccountTableCell.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';

import { useSelector } from 'react-redux';

import { getAccountById } from '../store/reducers/accounts';

function AccountTableCell({ id }) {
const account = useSelector((state) => getAccountById(state, { id }));
const fullName = [account.first_name, account.last_name].join(' ').trim() || '-';
return (
<tr>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10 flex items-center justify-center border rounded-full bg-gray-50 border-gray-200 text-gray-600">
{account.username.substring(0, 1).toUpperCase()}
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{account.username}
</div>
<div className="text-sm text-gray-500">
{account.email}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{fullName}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Active
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{(account.superuser) ? 'Superuser' : 'Member'}
</td>
</tr>
);
}

export default AccountTableCell;

AccountTableCell.propTypes = {
id: PropTypes.string.isRequired,
};
69 changes: 67 additions & 2 deletions src/pages/admin/UserManagement.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,74 @@
import React from 'react';
import React, { useState } from 'react';

import { useDispatch, useSelector } from 'react-redux';

import { listAccounts } from '../../store/actions/accounts';

import { getAccountsIdsByPage } from '../../store/reducers/accounts';

import * as icons from '../../icons';

import Button from '../../components/ui/Button';

import AccountTableCell from '../../components/AccountTableCell';

function UserManagement() {
const dispatch = useDispatch();
const [page] = useState(1);
const ids = useSelector((state) => getAccountsIdsByPage(state, { page }));
React.useEffect(() => {
if (ids == null) {
dispatch(listAccounts(page));
}
}, [ids, page, dispatch]);
return (
<div>
User Management
<div className="px-8 pt-4 flex items-center justify-between">
<h2 className="text-gray-900 truncate text-xl sm:text-3xl font-medium">
User Management
</h2>
<div className="ml-6 flex text-2xl items-center space-x-8">
<Button
as="div"
type="primary"
title="Uploads"
size="lg"
icon={<icons.Plus className="flex-shrink-0 w-5 h-5" />}
/>
</div>
</div>

<div className="flex flex-col px-8 pt-8">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Username
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Role
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{ids?.map((id) => (
<AccountTableCell key={id} id={id} />
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
}
Expand Down
13 changes: 13 additions & 0 deletions src/store/actions/accounts.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
export const types = {
LIST_ACCOUNTS: 'LIST_ACCOUNTS',
LIST_ACCOUNTS_SUCCESS: 'LIST_ACCOUNTS_SUCCESS',

RETRIEVE_CURRENT_ACCOUNT: 'RETRIEVE_CURRENT_ACCOUNT',
RETRIEVE_CURRENT_ACCOUNT_SUCCESS: 'RETRIEVE_CURRENT_ACCOUNT_SUCCESS',
RETRIEVE_CURRENT_ACCOUNT_FAILURE: 'RETRIEVE_CURRENT_ACCOUNT_FAILURE',
};

export const listAccounts = (page, perPage = 25) => ({
type: types.LIST_ACCOUNTS,
payload: { page, perPage },
});

export const listAccountsSuccess = (data) => ({
type: types.LIST_ACCOUNTS_SUCCESS,
payload: { ...data },
});

export const retrieveCurrentAccount = () => ({
type: types.RETRIEVE_CURRENT_ACCOUNT,
payload: null,
Expand Down
57 changes: 53 additions & 4 deletions src/store/reducers/accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,54 @@ import { combineReducers } from 'redux';

import { types } from '../actions/accounts';

function currentAccount(state = null, action) {
function normalize(items) {
const data = {};
items.forEach((item) => {
data[item.id] = item;
});
return data;
}

function accountsById(state = {}, action) {
switch (action.type) {
case types.LIST_ACCOUNTS_SUCCESS: {
const { results } = action.payload;
return {
...state,
...normalize(results),
};
}
case types.RETRIEVE_CURRENT_ACCOUNT_SUCCESS: {
const { account } = action.payload;
return {
...state,
[account.id]: account,
};
}
default:
return state;
}
}

function accountIdsByPage(state = {}, action) {
switch (action.type) {
case types.LIST_ACCOUNTS_SUCCESS: {
const { results, page } = action.payload;
return {
...state,
[page]: results.map((account) => account.id),
};
}
default:
return state;
}
}

function currentAccountId(state = null, action) {
switch (action.type) {
case types.RETRIEVE_CURRENT_ACCOUNT_SUCCESS: {
return action.payload.account;
const { account } = action.payload;
return account.id;
}
case types.RETRIEVE_CURRENT_ACCOUNT_FAILURE: {
return null;
Expand All @@ -16,8 +60,13 @@ function currentAccount(state = null, action) {
}

export default combineReducers({
currentAccount,
byId: accountsById,
byPage: accountIdsByPage,
currentAccountId,
});

export const getCurrentAccount = (state) => state.accounts.currentAccount;
export const getAccountById = (state, props) => state.accounts.byId[props.id];
export const getCurrentAccountId = (state) => state.accounts.currentAccountId;
export const getCurrentAccount = (state) => getAccountById(state, { id: [getCurrentAccountId(state)] });
export const getAccountsIdsByPage = (state, props) => state.accounts.byPage[props.page];
export const isAdmin = (state) => getCurrentAccount(state)?.superuser ?? false;
19 changes: 19 additions & 0 deletions src/store/sagas/accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ import { getAccessToken } from '../reducers/auth';

import { tryRequest, tryResponse } from './_try';

function* listAccounts({ payload }) {
const accessToken = yield select(getAccessToken);

const { page, perPage } = payload;
const request = api.get(`/accounts/list_all?page=${page}&per_page=${perPage}`, accessToken);
const [response, err] = yield tryRequest(request);
if (err != null) {
return;
}

const [data, parseErr] = yield tryResponse(response.json());
if (parseErr != null) {
return;
}

yield put(actions.listAccountsSuccess(data));
}

function* retrieveCurrentAccount() {
const accessToken = yield select(getAccessToken);

Expand All @@ -26,5 +44,6 @@ function* retrieveCurrentAccount() {
}

export default [
takeEvery(actions.types.LIST_ACCOUNTS, listAccounts),
takeEvery(actions.types.RETRIEVE_CURRENT_ACCOUNT, retrieveCurrentAccount),
];

0 comments on commit 3df0926

Please sign in to comment.