1
2import {
3 Accordion,
4 AccordionButton,
5 AccordionItem,
6 AccordionPanel,
7 Box,
8 HStack,
9 IconButton,
10 Table,
11 TableProps,
12 Tbody,
13 Td,
14 Text,
15 Th,
16 Thead,
17 Tooltip,
18 Tr,
19 useBreakpointValue,
20 VStack,
21} from '@chakra-ui/react';
22import classNames from 'classnames';
23import { statusColors } from 'constants/Settings';
24import { FC, Fragment, useState } from 'react';
25import { useTranslation } from 'react-i18next';
26import { OnlineBadge } from 'components/online-badge';
27import { OnlineStatus } from 'components/online-status';
28import { StatusBadge } from 'components/status-badge';
29import {
30 Pagination,
31 EmptySection,
32 AccordionArrowIcon,
33 handleSort,
34 EditIcon,
35 StatusSortSelect,
36 Sort,
37} from 'components/table';
38import { UsageSlider, UsageSliderCompact } from './usage-slider';
39import { fetchUsers, useUsers } from 'stores';
40import { ActionButtons } from './action-buttons';
41import { pageSizeManagers } from 'utils/userPreferenceStorage';
42import { useQuery } from '@tanstack/react-query';
43import { User } from 'types';
44import { queryIds } from 'constants/query-ids';
45
46type ExpandedIndex = number | number[];
47
48type UsersTableProps = {} & TableProps;
49
50export const UsersTable: FC<UsersTableProps> = (props) => {
51 const {
52 usersFilters: filters,
53 onEditingUser,
54 onCreateUser,
55 onFilterChange,
56 } = useUsers();
57 const { data } = useQuery({
58 queryKey: [queryIds.users],
59 queryFn: () => { return fetchUsers(filters) },
60 initialData: { total: 0, users: [] },
61 });
62 const { t } = useTranslation();
63 const [selectedRow, setSelectedRow] = useState<ExpandedIndex | undefined>(
64 undefined
65 );
66 const useTable = useBreakpointValue({ base: false, md: true });
67
68 const handleStatusFilter = (e: any) => {
69 onFilterChange({
70 status: e.target.value.length > 0 ? e.target.value : undefined,
71 });
72 };
73 const isFiltered = data?.total !== data?.users.length;
74 const toggleAccordion = (index: number) => {
75 setSelectedRow(index === selectedRow ? undefined : index);
76 };
77 return (
78 <Box id="users-table" overflowX={{ base: 'unset', md: 'unset' }}>
79 <Accordion
80 allowMultiple
81 display={{ base: 'block', md: 'none' }}
82 index={selectedRow}
83 >
84 <Table orientation="vertical" zIndex="docked" {...props}>
85 <Thead zIndex="docked" position="relative">
86 <Tr>
87 <Th
88 position="sticky"
89 minW="120px"
90 pl={4}
91 pr={4}
92 cursor={'pointer'}
93 onClick={handleSort.bind(null, filters, 'username', onFilterChange)}
94 >
95 <HStack>
96 <span>{t('users')}</span>
97 <Sort sort={filters.sort} column="username" />
98 </HStack>
99 </Th>
100 <Th
101 position="sticky"
102 minW="50px"
103 pl={0}
104 pr={0}
105 w="140px"
106 cursor={'pointer'}
107 >
108 <HStack spacing={0} position="relative">
109 <Text
110 position="absolute"
111 _dark={{
112 bg: 'gray.750',
113 }}
114 _light={{
115 bg: '#F9FAFB',
116 }}
117 userSelect="none"
118 pointerEvents="none"
119 zIndex={1}
120 w="100%"
121 >
122 {t('usersTable.status')}
123 {filters.status ? ': ' + filters.status : ''}
124 </Text>
125 <StatusSortSelect filters={filters} handleStatusFilter={handleStatusFilter} options={['active', 'disabled', 'limited', 'expired', 'on_hold']} />
126 </HStack>
127 </Th>
128 <Th
129 position="sticky"
130 minW="100px"
131 cursor={'pointer'}
132 pr={0}
133 onClick={handleSort.bind(null, filters, 'used_traffic', onFilterChange)}
134 >
135 <HStack>
136 <span>{t('usersTable.dataUsage')}</span>
137 <Sort sort={filters.sort} column="used_traffic" />
138 </HStack>
139 </Th>
140 <Th
141 position="sticky"
142 minW="32px"
143 w="32px"
144 p={0}
145 cursor={'pointer'}
146 ></Th>
147 </Tr>
148 </Thead>
149 <Tbody>
150 {!useTable &&
151 data?.users.map((user, i) => {
152 return (
153 <Fragment key={user.username}>
154 <Tr
155 onClick={toggleAccordion.bind(null, i)}
156 cursor="pointer"
157 >
158 <Td borderBottom={0} minW="100px" pl={4} pr={4}>
159 <div className="flex-status">
160 <OnlineBadge lastOnline={user.online_at} />
161 {user.username}
162 </div>
163 </Td>
164 <Td borderBottom={0} minW="50px" pl={0} pr={0}>
165 <StatusBadge
166 compact
167 showDetail={false}
168 expiryDate={user.expire}
169 status={user.status}
170 />
171 </Td>
172 <Td borderBottom={0} minW="100px" pr={0}>
173 <UsageSliderCompact
174 totalUsedTraffic={user.lifetime_used_traffic}
175 dataLimitResetStrategy={
176 user.data_limit_reset_strategy
177 }
178 used={user.used_traffic}
179 total={user.data_limit}
180 colorScheme={statusColors[user.status].bandWidthColor}
181 />
182 </Td>
183 <Td p={0} borderBottom={0} w="32px" minW="32px">
184 <AccordionArrowIcon
185 color="gray.600"
186 _dark={{
187 color: 'gray.400',
188 }}
189 transition="transform .2s ease-out"
190 transform={
191 selectedRow === i ? 'rotate(180deg)' : '0deg'
192 }
193 />
194 </Td>
195 </Tr>
196 <Tr
197 className="collapsible"
198 onClick={toggleAccordion.bind(null, i)}
199 >
200 <Td p={0} colSpan={4}>
201 <AccordionItem border={0}>
202 <AccordionButton display="none"></AccordionButton>
203 <AccordionPanel
204 border={0}
205 cursor="pointer"
206 px={6}
207 py={3}
208 >
209 <VStack justifyContent="space-between" spacing="4">
210 <VStack
211 alignItems="flex-start"
212 w="full"
213 spacing={-1}
214 >
215 <Text
216 textTransform="capitalize"
217 fontSize="xs"
218 fontWeight="bold"
219 color="gray.600"
220 _dark={{
221 color: 'gray.400',
222 }}
223 >
224 {t('usersTable.dataUsage')}
225 </Text>
226 <Box width="full" minW="230px">
227 <UsageSlider
228 totalUsedTraffic={
229 user.lifetime_used_traffic
230 }
231 dataLimitResetStrategy={
232 user.data_limit_reset_strategy
233 }
234 used={user.used_traffic}
235 total={user.data_limit}
236 colorScheme={
237 statusColors[user.status].bandWidthColor
238 }
239 />
240 </Box>
241 </VStack>
242 <HStack w="full" justifyContent="space-between">
243 <Box width="full">
244 <StatusBadge
245 compact
246 expiryDate={user.expire}
247 status={user.status}
248 />
249 <OnlineStatus lastOnline={user.online_at} />
250 </Box>
251 <HStack>
252 <ActionButtons user={user} />
253 <Tooltip
254 label={t('userDialog.editUser')}
255 placement="top"
256 >
257 <IconButton
258 p="0 !important"
259 aria-label="Edit user"
260 bg="transparent"
261 _dark={{
262 _hover: {
263 bg: 'gray.700',
264 },
265 }}
266 size={{
267 base: 'sm',
268 md: 'md',
269 }}
270 onClick={(e) => {
271 e.stopPropagation();
272 onEditingUser(user);
273 }}
274 >
275 <EditIcon />
276 </IconButton>
277 </Tooltip>
278 </HStack>
279 </HStack>
280 </VStack>
281 </AccordionPanel>
282 </AccordionItem>
283 </Td>
284 </Tr>
285 </Fragment>
286 );
287 })}
288 </Tbody>
289 </Table>
290 </Accordion>
291 <Table
292 orientation="vertical"
293 display={{ base: 'none', md: 'table' }}
294 {...props}
295 >
296 <Thead zIndex="docked" position="relative">
297 <Tr>
298 <Th
299 position="sticky"
300 minW="140px"
301 cursor={'pointer'}
302 onClick={handleSort.bind(null, filters, 'username', onFilterChange)}
303 >
304 <HStack>
305 <span>{t('username')}</span>
306 <Sort sort={filters.sort} column="username" />
307 </HStack>
308 </Th>
309 <Th
310 position="sticky"
311 width="400px"
312 minW="150px"
313 cursor={'pointer'}
314 >
315 <HStack spacing={0} position="relative">
316 <Text
317 position="absolute"
318 _dark={{
319 bg: 'gray.750',
320 }}
321 _light={{
322 bg: '#F9FAFB',
323 }}
324 userSelect="none"
325 pointerEvents="none"
326 zIndex={1}
327 w="100%"
328 >
329 {t('usersTable.status')}
330 {filters.status ? ': ' + filters.status : ''}
331 </Text>
332 <StatusSortSelect filters={filters} handleStatusFilter={handleStatusFilter} options={['active', 'disabled', 'limited', 'expired', 'on_hold']} />
333 </HStack>
334 </Th>
335 <Th
336 position="sticky"
337 width="350px"
338 minW="230px"
339 cursor={'pointer'}
340 onClick={handleSort.bind(null, filters, 'used_traffic', onFilterChange)}
341 >
342 <HStack>
343 <span>{t('usersTable.dataUsage')}</span>
344 <Sort sort={filters.sort} column="used_traffic" />
345 </HStack>
346 </Th>
347 <Th
348 position="sticky"
349 width="200px"
350 minW="180px"
351 />
352 </Tr>
353 </Thead>
354 <Tbody>
355 {useTable &&
356 data?.users.map((user: User, i) => {
357 return (
358 <Tr
359 key={user.username}
360 className={classNames('interactive', {
361 'last-row': i === data?.users.length - 1,
362 })}
363 onClick={() => onEditingUser(user)}
364 >
365 <Td minW="140px">
366 <div className="flex-status">
367 <OnlineBadge lastOnline={user.online_at} />
368 {user.username}
369 <OnlineStatus lastOnline={user.online_at} />
370 </div>
371 </Td>
372 <Td width="400px" minW="150px">
373 <StatusBadge
374 expiryDate={user.expire}
375 status={user.status}
376 />
377 </Td>
378 <Td width="350px" minW="230px">
379 <UsageSlider
380 totalUsedTraffic={user.lifetime_used_traffic}
381 dataLimitResetStrategy={user.data_limit_reset_strategy}
382 used={user.used_traffic}
383 total={user.data_limit}
384 colorScheme={statusColors[user.status].bandWidthColor}
385 />
386 </Td>
387 <Td width="200px" minW="180px">
388 <ActionButtons user={user} />
389 </Td>
390 </Tr>
391 );
392 })}
393 {data?.users.length == 0 && (
394 <Tr>
395 <Td colSpan={4}>
396 <EmptySection
397 isFiltered={isFiltered}
398 noObjectT="usersTable.noUser"
399 noObjectMatchedT='usersTable.noUserMatched'
400 createObjectT="createUser"
401 onCreateObject={onCreateUser}
402 />
403 </Td>
404 </Tr>
405 )}
406 </Tbody>
407 </Table>
408 <Pagination filters={filters} total={data?.total || 0} onFilterChange={onFilterChange} pageSizeManager={pageSizeManagers.users} />
409 </Box>
410 );
411};