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"