gitscope
← marzneshin / v0.1.2 / app/dashboard/src/pages/users/table/index.tsx
https://dev.kronoz.win/r/marzneshin.git
app/dashboard/src/pages/users/table/index.tsx · TypeScript
Blame Raw
  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 '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};