diff --git a/package.json b/package.json index 33e067e..700305f 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@emotion/styled": "^11.10.4", "@mui/icons-material": "^5.10.3", "@mui/material": "^5.10.3", + "@mui/x-data-grid": "^5.17.4", "@tauri-apps/api": "^1.1.0", "ahooks": "^3.7.0", "axios": "^0.27.2", diff --git a/src/components/connection/connection-item.tsx b/src/components/connection/connection-item.tsx index 01a9095..7f7e823 100644 --- a/src/components/connection/connection-item.tsx +++ b/src/components/connection/connection-item.tsx @@ -1,19 +1,25 @@ import dayjs from "dayjs"; import { useLockFn } from "ahooks"; -import { styled, ListItem, IconButton, ListItemText } from "@mui/material"; +import { + styled, + ListItem, + IconButton, + ListItemText, + Box, + alpha, +} from "@mui/material"; import { CloseRounded } from "@mui/icons-material"; import { deleteConnection } from "@/services/api"; import parseTraffic from "@/utils/parse-traffic"; const Tag = styled("span")(({ theme }) => ({ - display: "inline-block", - fontSize: "12px", + fontSize: "10px", padding: "0 4px", lineHeight: 1.375, - border: "1px solid #ccc", + border: "1px solid", borderRadius: 4, - marginRight: "0.1em", - transform: "scale(0.92)", + borderColor: alpha(theme.palette.text.secondary, 0.35), + marginRight: "4px", })); interface Props { @@ -26,7 +32,7 @@ const ConnectionItem = (props: Props) => { const { id, metadata, chains, start, curUpload, curDownload } = value; const onDelete = useLockFn(async () => deleteConnection(id)); - const showTraffic = curUpload! > 1024 || curDownload! > 1024; + const showTraffic = curUpload! >= 100 || curDownload! >= 100; return ( { sx={{ userSelect: "text" }} primary={metadata.host || metadata.destinationIP} secondary={ - <> + {metadata.network} {metadata.type} - {metadata.process && {metadata.process}} + {!!metadata.process && {metadata.process}} {chains.length > 0 && {chains[value.chains.length - 1]}} - {chains.length > 0 && {chains[0]}} - {dayjs(start).fromNow()} {showTraffic && ( @@ -61,7 +65,7 @@ const ConnectionItem = (props: Props) => { {parseTraffic(curUpload!)} / {parseTraffic(curDownload!)} )} - + } /> diff --git a/src/components/connection/connection-table.tsx b/src/components/connection/connection-table.tsx new file mode 100644 index 0000000..2111c81 --- /dev/null +++ b/src/components/connection/connection-table.tsx @@ -0,0 +1,144 @@ +import dayjs from "dayjs"; +import { useMemo } from "react"; +import { DataGrid, GridColDef } from "@mui/x-data-grid"; +import parseTraffic from "@/utils/parse-traffic"; + +interface Props { + connections: ApiType.ConnectionsItem[]; +} + +const ConnectionTable = (props: Props) => { + const { connections } = props; + + const columns: GridColDef[] = [ + { + field: "host", + headerName: "Host", + flex: 200, + minWidth: 200, + resizable: false, + disableColumnMenu: true, + }, + { + field: "download", + headerName: "Download", + width: 88, + align: "right", + headerAlign: "right", + disableColumnMenu: true, + valueFormatter: (params: any) => parseTraffic(params.value).join(" "), + }, + { + field: "upload", + headerName: "Upload", + width: 88, + align: "right", + headerAlign: "right", + disableColumnMenu: true, + valueFormatter: (params: any) => parseTraffic(params.value).join(" "), + }, + { + field: "dlSpeed", + headerName: "DL Speed", + align: "right", + width: 88, + headerAlign: "right", + disableColumnMenu: true, + valueFormatter: (params: any) => + parseTraffic(params.value).join(" ") + "/s", + }, + { + field: "ulSpeed", + headerName: "UL Speed", + width: 88, + align: "right", + headerAlign: "right", + disableColumnMenu: true, + valueFormatter: (params: any) => + parseTraffic(params.value).join(" ") + "/s", + }, + { + field: "chains", + headerName: "Chains", + width: 360, + disableColumnMenu: true, + }, + { + field: "rule", + headerName: "Rule", + width: 225, + disableColumnMenu: true, + }, + { + field: "process", + headerName: "Process", + width: 120, + disableColumnMenu: true, + }, + { + field: "time", + headerName: "Time", + width: 120, + align: "right", + headerAlign: "right", + disableColumnMenu: true, + valueFormatter: (params) => dayjs(params.value).fromNow(), + }, + { + field: "source", + headerName: "Source", + width: 150, + disableColumnMenu: true, + }, + { + field: "destinationIP", + headerName: "Destination IP", + width: 125, + disableColumnMenu: true, + }, + { + field: "type", + headerName: "Type", + width: 160, + disableColumnMenu: true, + }, + ]; + + const connRows = useMemo(() => { + return connections.map((each) => { + const { metadata, rulePayload } = each; + const chains = [...each.chains].reverse().join(" / "); + const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule; + + return { + id: each.id, + host: metadata.host + ? `${metadata.host}:${metadata.destinationPort}` + : `${metadata.destinationIP}:${metadata.destinationPort}`, + download: each.download, + upload: each.upload, + dlSpeed: each.curDownload, + ulSpeed: each.curUpload, + chains, + rule, + process: metadata.process || metadata.processPath, + time: each.start, + source: `${metadata.sourceIP}:${metadata.sourcePort}`, + destinationIP: metadata.destinationIP, + type: `${metadata.type}(${metadata.network})`, + }; + }); + }, [connections]); + + return ( + + ); +}; + +export default ConnectionTable; diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index ece2bfe..f85c54b 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -1,12 +1,24 @@ import { useEffect, useMemo, useState } from "react"; import { useLockFn } from "ahooks"; -import { Box, Button, MenuItem, Paper, Select, TextField } from "@mui/material"; +import { + Box, + Button, + IconButton, + MenuItem, + Paper, + Select, + TextField, +} from "@mui/material"; +import { useRecoilState } from "recoil"; import { Virtuoso } from "react-virtuoso"; import { useTranslation } from "react-i18next"; +import { TableChartRounded, TableRowsRounded } from "@mui/icons-material"; import { closeAllConnections, getInformation } from "@/services/api"; +import { atomConnectionSetting } from "@/services/states"; import BasePage from "@/components/base/base-page"; import BaseEmpty from "@/components/base/base-empty"; import ConnectionItem from "@/components/connection/connection-item"; +import ConnectionTable from "@/components/connection/connection-table"; const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] }; @@ -19,10 +31,12 @@ const ConnectionsPage = () => { const [curOrderOpt, setOrderOpt] = useState("Default"); const [connData, setConnData] = useState(initConn); + const [setting, setSetting] = useRecoilState(atomConnectionSetting); + + const isTableLayout = setting.layout === "table"; + const orderOpts: Record = { Default: (list) => list, - // "Download Traffic": (list) => list, - // "Upload Traffic": (list) => list, "Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!), "Download Speed": (list) => list.sort((a, b) => b.curDownload! - a.curDownload!), @@ -92,14 +106,29 @@ const ConnectionsPage = () => { title={t("Connections")} contentStyle={{ height: "100%" }} header={ - + + + setSetting((o) => + o.layout === "list" + ? { ...o, layout: "table" } + : { ...o, layout: "list" } + ) + } + > + {isTableLayout ? ( + + ) : ( + + )} + + + + } > @@ -113,23 +142,25 @@ const ConnectionsPage = () => { alignItems: "center", }} > - + {!isTableLayout && ( + + )} { - {filterConn.length > 0 ? ( + {filterConn.length === 0 ? ( + + ) : isTableLayout ? ( + + ) : ( } /> - ) : ( - )} diff --git a/src/services/states.ts b/src/services/states.ts index 664fc3b..48b66e8 100644 --- a/src/services/states.ts +++ b/src/services/states.ts @@ -21,14 +21,45 @@ export const atomEnableLog = atom({ ({ setSelf, onSet }) => { const key = "enable-log"; - setSelf(localStorage.getItem(key) !== "false"); + try { + setSelf(localStorage.getItem(key) !== "false"); + } catch {} onSet((newValue, _, isReset) => { - if (isReset) { - localStorage.removeItem(key); - } else { - localStorage.setItem(key, newValue.toString()); - } + try { + if (isReset) { + localStorage.removeItem(key); + } else { + localStorage.setItem(key, newValue.toString()); + } + } catch {} + }); + }, + ], +}); + +interface IConnectionSetting { + layout: "table" | "list"; +} + +export const atomConnectionSetting = atom({ + key: "atomConnectionSetting", + effects: [ + ({ setSelf, onSet }) => { + const key = "connections-setting"; + + try { + const value = localStorage.getItem(key); + const data = value == null ? { layout: "list" } : JSON.parse(value); + setSelf(data); + } catch { + setSelf({ layout: "list" }); + } + + onSet((newValue) => { + try { + localStorage.setItem(key, JSON.stringify(newValue)); + } catch {} }); }, ], diff --git a/src/services/types.d.ts b/src/services/types.d.ts index ab94609..7f8d868 100644 --- a/src/services/types.d.ts +++ b/src/services/types.d.ts @@ -68,6 +68,7 @@ declare namespace ApiType { destinationPort: string; destinationIP?: string; process?: string; + processPath?: string; }; upload: number; download: number; diff --git a/yarn.lock b/yarn.lock index 57e25d6..5f5c5b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -552,6 +552,17 @@ prop-types "^15.8.1" react-is "^18.2.0" +"@mui/x-data-grid@^5.17.4": + version "5.17.4" + resolved "https://registry.yarnpkg.com/@mui/x-data-grid/-/x-data-grid-5.17.4.tgz#93ccd06a0a15d02b8d59c2d3038e217ffc72350d" + integrity sha512-cxZuu65Whh1DNU9M2X5ljDOx+GAEpGeJLPnugMjhgqTOszfJZX/4kI7NftrPy051Hy0um0sv0NVTDSFXG6yixA== + dependencies: + "@babel/runtime" "^7.18.9" + "@mui/utils" "^5.10.3" + clsx "^1.2.1" + prop-types "^15.8.1" + reselect "^4.1.6" + "@octokit/auth-token@^2.4.4": version "2.5.0" resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36" @@ -2016,6 +2027,11 @@ regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== +reselect@^4.1.6: + version "4.1.6" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.6.tgz#19ca2d3d0b35373a74dc1c98692cdaffb6602656" + integrity sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ== + resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"