connections.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import { useEffect, useMemo, useState } from "react";
  2. import { useLockFn } from "ahooks";
  3. import {
  4. Box,
  5. Button,
  6. IconButton,
  7. MenuItem,
  8. Paper,
  9. Select,
  10. TextField,
  11. } from "@mui/material";
  12. import { useRecoilState } from "recoil";
  13. import { Virtuoso } from "react-virtuoso";
  14. import { useTranslation } from "react-i18next";
  15. import { TableChartRounded, TableRowsRounded } from "@mui/icons-material";
  16. import { closeAllConnections, getInformation } from "@/services/api";
  17. import { atomConnectionSetting } from "@/services/states";
  18. import BasePage from "@/components/base/base-page";
  19. import BaseEmpty from "@/components/base/base-empty";
  20. import ConnectionItem from "@/components/connection/connection-item";
  21. import ConnectionTable from "@/components/connection/connection-table";
  22. const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
  23. type OrderFunc = (list: ApiType.ConnectionsItem[]) => ApiType.ConnectionsItem[];
  24. const ConnectionsPage = () => {
  25. const { t, i18n } = useTranslation();
  26. const [filterText, setFilterText] = useState("");
  27. const [curOrderOpt, setOrderOpt] = useState("Default");
  28. const [connData, setConnData] = useState<ApiType.Connections>(initConn);
  29. const [setting, setSetting] = useRecoilState(atomConnectionSetting);
  30. const isTableLayout = setting.layout === "table";
  31. const orderOpts: Record<string, OrderFunc> = {
  32. Default: (list) => list,
  33. "Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!),
  34. "Download Speed": (list) =>
  35. list.sort((a, b) => b.curDownload! - a.curDownload!),
  36. };
  37. const filterConn = useMemo(() => {
  38. const orderFunc = orderOpts[curOrderOpt];
  39. const connetions = connData.connections.filter((conn) =>
  40. (conn.metadata.host || conn.metadata.destinationIP)?.includes(filterText)
  41. );
  42. if (orderFunc) return orderFunc(connetions);
  43. return connetions;
  44. }, [connData, filterText, curOrderOpt]);
  45. useEffect(() => {
  46. let ws: WebSocket | null = null;
  47. getInformation().then((result) => {
  48. const { server = "", secret = "" } = result;
  49. ws = new WebSocket(`ws://${server}/connections?token=${secret}`);
  50. ws.addEventListener("message", (event) => {
  51. const data = JSON.parse(event.data) as ApiType.Connections;
  52. // 与前一次connections的展示顺序尽量保持一致
  53. setConnData((old) => {
  54. const oldConn = old.connections;
  55. const maxLen = data.connections.length;
  56. const connections: typeof oldConn = [];
  57. const rest = data.connections.filter((each) => {
  58. const index = oldConn.findIndex((o) => o.id === each.id);
  59. if (index >= 0 && index < maxLen) {
  60. const old = oldConn[index];
  61. each.curUpload = each.upload - old.upload;
  62. each.curDownload = each.download - old.download;
  63. connections[index] = each;
  64. return false;
  65. }
  66. return true;
  67. });
  68. for (let i = 0; i < maxLen; ++i) {
  69. if (!connections[i] && rest.length > 0) {
  70. connections[i] = rest.shift()!;
  71. connections[i].curUpload = 0;
  72. connections[i].curDownload = 0;
  73. }
  74. }
  75. return { ...data, connections };
  76. });
  77. });
  78. });
  79. return () => ws?.close();
  80. }, []);
  81. const onCloseAll = useLockFn(closeAllConnections);
  82. return (
  83. <BasePage
  84. title={t("Connections")}
  85. contentStyle={{ height: "100%" }}
  86. header={
  87. <Box sx={{ mt: 1, display: "flex", alignItems: "center" }}>
  88. <IconButton
  89. size="small"
  90. sx={{ mr: 2 }}
  91. onClick={() =>
  92. setSetting((o) =>
  93. o.layout === "list"
  94. ? { ...o, layout: "table" }
  95. : { ...o, layout: "list" }
  96. )
  97. }
  98. >
  99. {isTableLayout ? (
  100. <TableChartRounded fontSize="inherit" />
  101. ) : (
  102. <TableRowsRounded fontSize="inherit" />
  103. )}
  104. </IconButton>
  105. <Button size="small" variant="contained" onClick={onCloseAll}>
  106. {t("Close All")}
  107. </Button>
  108. </Box>
  109. }
  110. >
  111. <Paper sx={{ boxShadow: 2, height: "100%" }}>
  112. <Box
  113. sx={{
  114. pt: 1,
  115. mb: 0.5,
  116. mx: "12px",
  117. height: "36px",
  118. display: "flex",
  119. alignItems: "center",
  120. }}
  121. >
  122. {!isTableLayout && (
  123. <Select
  124. size="small"
  125. autoComplete="off"
  126. value={curOrderOpt}
  127. onChange={(e) => setOrderOpt(e.target.value)}
  128. sx={{
  129. mr: 1,
  130. width: i18n.language === "en" ? 190 : 120,
  131. '[role="button"]': { py: 0.65 },
  132. }}
  133. >
  134. {Object.keys(orderOpts).map((opt) => (
  135. <MenuItem key={opt} value={opt}>
  136. <span style={{ fontSize: 14 }}>{t(opt)}</span>
  137. </MenuItem>
  138. ))}
  139. </Select>
  140. )}
  141. <TextField
  142. hiddenLabel
  143. fullWidth
  144. size="small"
  145. autoComplete="off"
  146. variant="outlined"
  147. placeholder={t("Filter conditions")}
  148. value={filterText}
  149. onChange={(e) => setFilterText(e.target.value)}
  150. sx={{ input: { py: 0.65, px: 1.25 } }}
  151. />
  152. </Box>
  153. <Box height="calc(100% - 50px)">
  154. {filterConn.length === 0 ? (
  155. <BaseEmpty text="No Connections" />
  156. ) : isTableLayout ? (
  157. <ConnectionTable connections={filterConn} />
  158. ) : (
  159. <Virtuoso
  160. data={filterConn}
  161. itemContent={(index, item) => <ConnectionItem value={item} />}
  162. />
  163. )}
  164. </Box>
  165. </Paper>
  166. </BasePage>
  167. );
  168. };
  169. export default ConnectionsPage;