gitscope
← marzneshin / v0.1.2 / app/dashboard/src/pages/nodes/table/index.tsx
https://dev.kronoz.win/r/marzneshin.git
app/dashboard/src/pages/nodes/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 { ActionButtons } from './action-buttons';
 24import { pageSizeManagers } from 'utils/userPreferenceStorage';
 25import { FC, Fragment, useState } from 'react';
 26import { useTranslation } from 'react-i18next';
 27import { StatusBadge } from 'components/status-badge';
 28import {
 29  Pagination,
 30  EmptySection,
 31  AccordionArrowIcon,
 32  EditIcon,
 33  Sort,
 34  StatusSortSelect,
 35  handleSort,
 36} from 'components/table';
 37import { fetchNodes, useNodes } from 'stores';
 38import { useQuery } from 'react-query';
 39import { queryIds } from 'constants/query-ids';
 40
 41type ExpandedIndex = number | number[];
 42
 43type NodesTableProps = {} & TableProps;
 44
 45export const NodesTable: FC<NodesTableProps> = (props) => {
 46  const {
 47    nodesFilters: filters,
 48    onEditingNode,
 49    onAddingNode,
 50    onFilterChange,
 51  } = useNodes();
 52  const { data: nodes } = useQuery(queryIds.nodes, () => { return fetchNodes() })
 53  const total = nodes?.length || 0;
 54
 55  const { t } = useTranslation();
 56  const [selectedRow, setSelectedRow] = useState<ExpandedIndex | undefined>(
 57    undefined
 58  );
 59
 60  const useTable = useBreakpointValue({ base: false, md: true });
 61
 62  // TODO: Find a different mechancism to to detect ftiler
 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
 75  const StatusSortSelectNodes = () => (
 76    <StatusSortSelect
 77      filters={filters}
 78      options={['healthy', 'unhealthy', 'disabled']}
 79      handleStatusFilter={handleStatusFilter}
 80    />
 81  );
 82
 83  return (
 84    <Box id="node-table" overflowX={{ base: 'unset', md: 'unset' }} >
 85      <Accordion
 86        allowMultiple
 87        display={{ base: 'block', md: 'none' }}
 88        index={selectedRow}
 89      >
 90        <Table orientation="vertical" zIndex="docked" {...props}>
 91          <Thead zIndex="docked" position="relative">
 92            <Tr>
 93              <Th
 94                position="fixed"
 95                minW="120px"
 96                pl={4}
 97                pr={4}
 98                cursor={'pointer'}
 99                onClick={handleSort.bind(null, filters, 'name', onFilterChange)}
100              >
101                <HStack>
102                  <span>{t('name')}</span>
103                  <Sort sort={filters.sort} column="name" />
104                </HStack>
105              </Th>
106              <Th
107                position="fixed"
108                minW="50px"
109                pl={0}
110                pr={0}
111                w="140px"
112                cursor={'pointer'}
113              >
114                <HStack spacing={0} position="relative">
115                  <Text
116                    position="absolute"
117                    _dark={{
118                      bg: 'gray.750',
119                    }}
120                    _light={{
121                      bg: '#F9FAFB',
122                    }}
123                    userSelect="none"
124                    pointerEvents="none"
125                    zIndex={1}
126                    w="100%"
127                  >
128                    {t('nodesTable.status')}
129                    {filters.status ? ': ' + filters.status : ''}
130                  </Text>
131                  <StatusSortSelectNodes />
132                </HStack>
133              </Th>
134              <Th
135                position="fixed"
136                minW="100px"
137                cursor={'pointer'}
138                pr={0}
139                onClick={handleSort.bind(null, filters, 'xray_version', onFilterChange)}
140              >
141                <HStack>
142                  <span>{t('nodesTable.xrayVersion')}</span>
143                  <Sort sort={filters.sort} column="xray_version" />
144                </HStack>
145              </Th>
146              <Th
147                position="fixed"
148                minW="100px"
149                cursor={'pointer'}
150                pr={0}
151              >
152                <span>{t('nodesTable.ipPort')}</span>
153              </Th>
154              {/* Action buttons (certificate, core settings)*/}
155              <Th
156                position="fixed"
157                minW="32px"
158                w="32px"
159                p={0}
160                cursor={'pointer'}
161              ></Th>
162            </Tr>
163          </Thead>
164          <Tbody>
165            {!useTable &&
166              nodes?.map((node, i) => {
167                return (
168                  <Fragment key={node.name}>
169                    <Tr
170                      onClick={toggleAccordion.bind(null, i)}
171                      cursor="pointer"
172                    >
173                      <Td borderBottom={0} minW="100px" pl={4} pr={4}>
174                        {node.name}
175                      </Td>
176                      <Td borderBottom={0} minW="50px" pl={0} pr={0}>
177                        <StatusBadge
178                          compact
179                          showDetail={false}
180                          status={node.status}
181                        />
182                      </Td>
183                      <Td borderBottom={0} minW="100px" pr={0}>
184                        {node.xray_version}
185                      </Td>
186                      <Td p={0} borderBottom={0} w="32px" minW="32px">
187                        <AccordionArrowIcon
188                          color="gray.600"
189                          _dark={{
190                            color: 'gray.400',
191                          }}
192                          transition="transform .2s ease-out"
193                          transform={
194                            selectedRow === i ? 'rotate(180deg)' : '0deg'
195                          }
196                        />
197                      </Td>
198                    </Tr>
199                    <Tr
200                      className="collapsible"
201                      onClick={toggleAccordion.bind(null, i)}
202                    >
203                      <Td p={0} colSpan={4}>
204                        <AccordionItem border={0}>
205                          <AccordionButton display="none"></AccordionButton>
206                          <AccordionPanel
207                            border={0}
208                            cursor="pointer"
209                            px={6}
210                            py={3}
211                          >
212                            <VStack justifyContent="space-between" spacing="4">
213                              <VStack
214                                alignItems="flex-start"
215                                w="full"
216                                spacing={-1}
217                              >
218                                <Text
219                                  textTransform="capitalize"
220                                  fontSize="xs"
221                                  fontWeight="bold"
222                                  color="gray.600"
223                                  _dark={{
224                                    color: 'gray.400',
225                                  }}
226                                >
227                                  {t('nodesTable.ipPort')}
228                                </Text>
229                                <Box width="full" minW="230px">
230                                  {node.address}:{node.port}
231                                </Box>
232                              </VStack>
233                              <HStack w="full" justifyContent="space-between">
234                                <HStack>
235                                  <ActionButtons />
236                                  <Tooltip
237                                    label={t('nodesDialog.editNode')}
238                                    placement="top"
239                                  >
240                                    <IconButton
241                                      p="0 !important"
242                                      aria-label="Edit user"
243                                      bg="transparent"
244                                      _dark={{
245                                        _hover: {
246                                          bg: 'gray.700',
247                                        },
248                                      }}
249                                      size={{
250                                        base: 'sm',
251                                        md: 'md',
252                                      }}
253                                      onClick={(e) => {
254                                        e.stopPropagation();
255                                        onEditingNode(node);
256                                      }}
257                                    >
258                                      <EditIcon />
259                                    </IconButton>
260                                  </Tooltip>
261                                </HStack>
262                              </HStack>
263                            </VStack>
264                          </AccordionPanel>
265                        </AccordionItem>
266                      </Td>
267                    </Tr>
268                  </Fragment>
269                );
270              })}
271          </Tbody>
272        </Table>
273      </Accordion>
274      <Table
275        orientation="vertical"
276        display={{ base: 'none', md: 'table' }}
277        {...props}
278      >
279        <Thead zIndex="docked" position="relative">
280          <Tr>
281            <Th
282              minW="200px"
283              cursor={'pointer'}
284              onClick={handleSort.bind(null, filters, 'name', onFilterChange)}
285            >
286              <HStack>
287                <span>{t('name')}</span>
288                <Sort sort={filters.sort} column="name" />
289              </HStack>
290            </Th>
291            <Th
292              width="100px"
293              minW="70px"
294              cursor={'pointer'}
295            >
296              <HStack spacing={0} position="relative">
297                <Text
298                  position="absolute"
299                  _dark={{
300                    bg: 'gray.750',
301                  }}
302                  _light={{
303                    bg: '#F9FAFB',
304                  }}
305                  userSelect="none"
306                  pointerEvents="none"
307                  zIndex={1}
308                  w="100%"
309                >
310                  {t('nodesTable.status')}
311                  {filters.status ? ': ' + filters.status : ''}
312                </Text>
313                <StatusSortSelectNodes />
314              </HStack>
315            </Th>
316            <Th
317              width="100px"
318              minW="70px"
319              cursor={'pointer'}
320              onClick={handleSort.bind(null, filters, 'xray_version', onFilterChange)}
321            >
322              <HStack>
323                <span>{t('nodesTable.xrayVersion')}</span>
324                <Sort sort={filters.sort} column="xray_version" />
325              </HStack>
326            </Th>
327            <Th
328              width="100px"
329              minW="70px"
330              cursor={'pointer'}
331              onClick={handleSort.bind(null, filters, 'address', onFilterChange)}
332            >
333              <HStack>
334                <span>{t('nodesTable.ipPort')}</span>
335                <Sort sort={filters.sort} column="address" />
336              </HStack>
337            </Th>
338            <Th
339              width="60px"
340              minW="50px"
341            />
342          </Tr>
343        </Thead>
344        <Tbody overflowY="auto">
345          {useTable &&
346            nodes?.map((node, i) => {
347              return (
348                <Tr
349                  key={node.name}
350                  className={classNames('interactive', {
351                    'last-row': i === nodes.length - 1,
352                  })}
353                  onClick={() => onEditingNode(node)}
354                >
355                  <Td minW="140px">
356                    <div className="flex-status">
357                      {node.name}
358                    </div>
359                  </Td>
360                  <Td width="400px" minW="150px">
361                    <StatusBadge
362                      status={node.status}
363                    />
364                  </Td>
365                  <Td width="350px" minW="150px">
366                    {node.xray_version}
367                  </Td>
368                  <Td width="350px" minW="150px">
369                    {node.address}:{node.port}
370                  </Td>
371                  <Td width="200px" minW="180px">
372                    <ActionButtons />
373                  </Td>
374                </Tr>
375              );
376            })}
377          {nodes?.length == 0 && (
378            <Tr>
379              <Td colSpan={5}>
380                <EmptySection
381                  isFiltered={/* isFiltered */ false}
382                  noObjectT="nodesTable.noNode"
383                  noObjectMatchedT='nodesTable.noNodeMatched'
384                  createObjectT="addNode"
385                  onCreateObject={onAddingNode}
386                />
387              </Td>
388            </Tr>
389          )}
390        </Tbody>
391      </Table>
392      <Pagination filters={filters} total={total} onFilterChange={onFilterChange} pageSizeManager={pageSizeManagers.nodes} />
393    </Box>
394  );
395};