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