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