ui/src/pages/Admin/Users/index.tsx (300 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { FC, useEffect, useState } from 'react';
import { Form, Table, Button, Stack } from 'react-bootstrap';
import { useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import {
Pagination,
FormatTime,
BaseUserCard,
Empty,
QueryGroup,
Modal,
} from '@/components';
import * as Type from '@/common/interface';
import { useUserModal } from '@/hooks';
import { toastStore, loggedUserInfoStore, userCenterStore } from '@/stores';
import {
useQueryUsers,
addUsers,
getAdminUcAgent,
AdminUcAgent,
changeUserStatus,
deletePermanently,
} from '@/services';
import { formatCount } from '@/utils';
import DeleteUserModal from './components/DeleteUserModal';
import Action from './components/Action';
const UserFilterKeys: Type.UserFilterBy[] = [
'normal',
'staff',
'inactive',
'suspended',
'deleted',
];
const bgMap = {
normal: 'text-bg-success',
suspended: 'text-bg-danger',
deleted: 'text-bg-danger',
inactive: 'text-bg-secondary',
};
const PAGE_SIZE = 10;
const Users: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });
const [deleteUserModalState, setDeleteUserModalState] = useState({
show: false,
userId: '',
});
const [urlSearchParams, setUrlSearchParams] = useSearchParams();
const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0];
const curPage = Number(urlSearchParams.get('page') || '1');
const curQuery = urlSearchParams.get('query') || '';
const currentUser = loggedUserInfoStore((state) => state.user);
const { agent: ucAgent } = userCenterStore();
const [adminUcAgent, setAdminUcAgent] = useState<AdminUcAgent>({
allow_create_user: true,
allow_update_user_status: true,
allow_update_user_password: true,
allow_update_user_role: true,
});
const {
data,
isLoading,
mutate: refreshUsers,
} = useQueryUsers({
page: curPage,
page_size: PAGE_SIZE,
query: curQuery,
...(curFilter === 'all'
? {}
: curFilter === 'staff'
? { staff: true }
: { status: curFilter }),
});
const userModal = useUserModal({
onConfirm: (userModel) => {
return new Promise((resolve, reject) => {
addUsers(userModel)
.then(() => {
if (/all|staff/.test(curFilter) && curPage === 1) {
refreshUsers();
}
resolve(true);
})
.catch((e) => {
reject(e);
});
});
},
});
const handleFilter = (e) => {
urlSearchParams.set('query', e.target.value);
urlSearchParams.delete('page');
setUrlSearchParams(urlSearchParams);
};
useEffect(() => {
if (ucAgent?.enabled) {
getAdminUcAgent().then((resp) => {
setAdminUcAgent(resp);
});
}
}, [ucAgent]);
const changeDeleteUserModalState = (modalData: {
show: boolean;
userId: string;
}) => {
setDeleteUserModalState(modalData);
};
const handleDelete = (val) => {
changeUserStatus({
user_id: deleteUserModalState.userId,
status: 'deleted',
remove_all_content: val,
}).then(() => {
toastStore.getState().show({
msg: t('user_deleted', { keyPrefix: 'messages' }),
variant: 'success',
});
changeDeleteUserModalState({
show: false,
userId: '',
});
refreshUsers();
});
};
const handleDeletePermanently = () => {
Modal.confirm({
title: t('title', { keyPrefix: 'delete_permanently' }),
content: t('content', { keyPrefix: 'delete_permanently' }),
cancelBtnVariant: 'link',
confirmText: t('delete', { keyPrefix: 'btns' }),
confirmBtnVariant: 'danger',
onConfirm: () => {
deletePermanently('users').then(() => {
toastStore.getState().show({
msg: t('users_deleted', { keyPrefix: 'messages' }),
variant: 'success',
});
refreshUsers();
});
},
});
};
const showAddUser =
!ucAgent?.enabled || (ucAgent?.enabled && adminUcAgent?.allow_create_user);
const showActionPassword =
!ucAgent?.enabled ||
(ucAgent?.enabled && adminUcAgent?.allow_update_user_password);
const showActionRole =
!ucAgent?.enabled ||
(ucAgent?.enabled && adminUcAgent?.allow_update_user_role);
const showActionStatus =
!ucAgent?.enabled ||
(ucAgent?.enabled && adminUcAgent?.allow_update_user_status);
const showAction = showActionPassword || showActionRole || showActionStatus;
return (
<>
<h3 className="mb-4">{t('title')}</h3>
<div className="d-flex flex-wrap justify-content-between align-items-center">
<Stack direction="horizontal" gap={3} className="mb-3">
<QueryGroup
data={UserFilterKeys}
currentSort={curFilter}
sortKey="filter"
i18nKeyPrefix="admin.users"
/>
{curFilter === 'deleted' && Number(data?.count) > 0 ? (
<Button
variant="outline-danger"
size="sm"
onClick={() => handleDeletePermanently()}>
{t('deleted_permanently', { keyPrefix: 'btns' })}
</Button>
) : null}
{showAddUser ? (
<Button
variant="outline-primary"
size="sm"
onClick={() => userModal.onShow()}>
{t('add_user')}
</Button>
) : null}
</Stack>
<Form.Control
size="sm"
type="search"
value={curQuery}
onChange={handleFilter}
placeholder={t('filter.placeholder')}
style={{ width: '12.25rem' }}
className="mb-3"
/>
</div>
<Table responsive="md">
<thead>
<tr>
<th>{t('name')}</th>
<th style={{ width: '12%' }}>{t('reputation')}</th>
<th style={{ width: '20%' }} className="min-w-15">
{t('email')}
</th>
<th className="text-nowrap" style={{ width: '15%' }}>
{t('created_at')}
</th>
{(curFilter === 'deleted' || curFilter === 'suspended') && (
<th className="text-nowrap" style={{ width: '15%' }}>
{curFilter === 'deleted' ? t('delete_at') : t('suspend_at')}
</th>
)}
<th style={{ width: '12%' }}>{t('status')}</th>
{curFilter !== 'suspended' && curFilter !== 'deleted' && (
<th style={{ width: '12%' }}>{t('role')}</th>
)}
{curFilter !== 'deleted' ? (
<th style={{ width: '8%' }} className="text-end">
{t('action')}
</th>
) : null}
</tr>
</thead>
<tbody className="align-middle">
{data?.list.map((user) => {
return (
<tr key={user.user_id}>
<td>
<BaseUserCard
data={user}
className="fs-6"
avatarSize="32px"
avatarSearchStr="s=48"
avatarClass="me-2"
showReputation={false}
nameMaxWidth="160px"
/>
</td>
<td>{formatCount(user.rank)}</td>
<td className="text-break">{user.e_mail}</td>
<td>
<FormatTime time={user.created_at} />
</td>
{curFilter === 'suspended' && (
<td className="text-nowrap">
<FormatTime time={user.suspended_at} />
</td>
)}
{curFilter === 'deleted' && (
<td className="text-nowrap">
<FormatTime time={user.deleted_at} />
</td>
)}
<td>
<span className={classNames('badge', bgMap[user.status])}>
{t(user.status)}
</span>
</td>
{curFilter !== 'suspended' && curFilter !== 'deleted' && (
<td>
<span className="badge text-bg-light">
{t(user.role_name)}
</span>
</td>
)}
{curFilter !== 'deleted' &&
(showAction || user.status === 'inactive') ? (
<Action
userData={user}
showActionPassword={showActionPassword}
showActionRole={showActionRole}
showActionStatus={showActionStatus}
currentUser={currentUser}
refreshUsers={refreshUsers}
showDeleteModal={changeDeleteUserModalState}
/>
) : null}
</tr>
);
})}
</tbody>
</Table>
{Number(data?.count) <= 0 && !isLoading && <Empty />}
<div className="mt-4 mb-2 d-flex justify-content-center">
<Pagination
currentPage={curPage}
totalSize={data?.count || 0}
pageSize={PAGE_SIZE}
/>
</div>
<DeleteUserModal
show={deleteUserModalState.show}
onClose={() => {
changeDeleteUserModalState({
show: false,
userId: '',
});
}}
onDelete={(val) => handleDelete(val)}
/>
</>
);
};
export default Users;