feat: show connections with table layout

This commit is contained in:
GyDi 2022-09-25 01:35:21 +08:00
parent 0495062110
commit 5a743779e2
No known key found for this signature in database
GPG Key ID: 58B15242BA8277A6
7 changed files with 279 additions and 49 deletions

View File

@ -22,6 +22,7 @@
"@emotion/styled": "^11.10.4", "@emotion/styled": "^11.10.4",
"@mui/icons-material": "^5.10.3", "@mui/icons-material": "^5.10.3",
"@mui/material": "^5.10.3", "@mui/material": "^5.10.3",
"@mui/x-data-grid": "^5.17.4",
"@tauri-apps/api": "^1.1.0", "@tauri-apps/api": "^1.1.0",
"ahooks": "^3.7.0", "ahooks": "^3.7.0",
"axios": "^0.27.2", "axios": "^0.27.2",

View File

@ -1,19 +1,25 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useLockFn } from "ahooks"; 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 { CloseRounded } from "@mui/icons-material";
import { deleteConnection } from "@/services/api"; import { deleteConnection } from "@/services/api";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
const Tag = styled("span")(({ theme }) => ({ const Tag = styled("span")(({ theme }) => ({
display: "inline-block", fontSize: "10px",
fontSize: "12px",
padding: "0 4px", padding: "0 4px",
lineHeight: 1.375, lineHeight: 1.375,
border: "1px solid #ccc", border: "1px solid",
borderRadius: 4, borderRadius: 4,
marginRight: "0.1em", borderColor: alpha(theme.palette.text.secondary, 0.35),
transform: "scale(0.92)", marginRight: "4px",
})); }));
interface Props { interface Props {
@ -26,7 +32,7 @@ const ConnectionItem = (props: Props) => {
const { id, metadata, chains, start, curUpload, curDownload } = value; const { id, metadata, chains, start, curUpload, curDownload } = value;
const onDelete = useLockFn(async () => deleteConnection(id)); const onDelete = useLockFn(async () => deleteConnection(id));
const showTraffic = curUpload! > 1024 || curDownload! > 1024; const showTraffic = curUpload! >= 100 || curDownload! >= 100;
return ( return (
<ListItem <ListItem
@ -41,19 +47,17 @@ const ConnectionItem = (props: Props) => {
sx={{ userSelect: "text" }} sx={{ userSelect: "text" }}
primary={metadata.host || metadata.destinationIP} primary={metadata.host || metadata.destinationIP}
secondary={ secondary={
<> <Box sx={{ display: "flex", flexWrap: "wrap" }}>
<Tag sx={{ textTransform: "uppercase", color: "success" }}> <Tag sx={{ textTransform: "uppercase", color: "success" }}>
{metadata.network} {metadata.network}
</Tag> </Tag>
<Tag>{metadata.type}</Tag> <Tag>{metadata.type}</Tag>
{metadata.process && <Tag>{metadata.process}</Tag>} {!!metadata.process && <Tag>{metadata.process}</Tag>}
{chains.length > 0 && <Tag>{chains[value.chains.length - 1]}</Tag>} {chains.length > 0 && <Tag>{chains[value.chains.length - 1]}</Tag>}
{chains.length > 0 && <Tag>{chains[0]}</Tag>}
<Tag>{dayjs(start).fromNow()}</Tag> <Tag>{dayjs(start).fromNow()}</Tag>
{showTraffic && ( {showTraffic && (
@ -61,7 +65,7 @@ const ConnectionItem = (props: Props) => {
{parseTraffic(curUpload!)} / {parseTraffic(curDownload!)} {parseTraffic(curUpload!)} / {parseTraffic(curDownload!)}
</Tag> </Tag>
)} )}
</> </Box>
} }
/> />
</ListItem> </ListItem>

View File

@ -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 (
<DataGrid
rows={connRows}
columns={columns}
density="compact"
sx={{ border: "none", "div:focus": { outline: "none !important" } }}
hideFooter
/>
);
};
export default ConnectionTable;

View File

@ -1,12 +1,24 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useLockFn } from "ahooks"; 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 { Virtuoso } from "react-virtuoso";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TableChartRounded, TableRowsRounded } from "@mui/icons-material";
import { closeAllConnections, getInformation } from "@/services/api"; import { closeAllConnections, getInformation } from "@/services/api";
import { atomConnectionSetting } from "@/services/states";
import BasePage from "@/components/base/base-page"; import BasePage from "@/components/base/base-page";
import BaseEmpty from "@/components/base/base-empty"; import BaseEmpty from "@/components/base/base-empty";
import ConnectionItem from "@/components/connection/connection-item"; import ConnectionItem from "@/components/connection/connection-item";
import ConnectionTable from "@/components/connection/connection-table";
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] }; const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
@ -19,10 +31,12 @@ const ConnectionsPage = () => {
const [curOrderOpt, setOrderOpt] = useState("Default"); const [curOrderOpt, setOrderOpt] = useState("Default");
const [connData, setConnData] = useState<ApiType.Connections>(initConn); const [connData, setConnData] = useState<ApiType.Connections>(initConn);
const [setting, setSetting] = useRecoilState(atomConnectionSetting);
const isTableLayout = setting.layout === "table";
const orderOpts: Record<string, OrderFunc> = { const orderOpts: Record<string, OrderFunc> = {
Default: (list) => list, Default: (list) => list,
// "Download Traffic": (list) => list,
// "Upload Traffic": (list) => list,
"Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!), "Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!),
"Download Speed": (list) => "Download Speed": (list) =>
list.sort((a, b) => b.curDownload! - a.curDownload!), list.sort((a, b) => b.curDownload! - a.curDownload!),
@ -92,14 +106,29 @@ const ConnectionsPage = () => {
title={t("Connections")} title={t("Connections")}
contentStyle={{ height: "100%" }} contentStyle={{ height: "100%" }}
header={ header={
<Button <Box sx={{ mt: 1, display: "flex", alignItems: "center" }}>
size="small" <IconButton
sx={{ mt: 1 }} size="small"
variant="contained" sx={{ mr: 2 }}
onClick={onCloseAll} onClick={() =>
> setSetting((o) =>
{t("Close All")} o.layout === "list"
</Button> ? { ...o, layout: "table" }
: { ...o, layout: "list" }
)
}
>
{isTableLayout ? (
<TableChartRounded fontSize="inherit" />
) : (
<TableRowsRounded fontSize="inherit" />
)}
</IconButton>
<Button size="small" variant="contained" onClick={onCloseAll}>
{t("Close All")}
</Button>
</Box>
} }
> >
<Paper sx={{ boxShadow: 2, height: "100%" }}> <Paper sx={{ boxShadow: 2, height: "100%" }}>
@ -113,23 +142,25 @@ const ConnectionsPage = () => {
alignItems: "center", alignItems: "center",
}} }}
> >
<Select {!isTableLayout && (
size="small" <Select
autoComplete="off" size="small"
value={curOrderOpt} autoComplete="off"
onChange={(e) => setOrderOpt(e.target.value)} value={curOrderOpt}
sx={{ onChange={(e) => setOrderOpt(e.target.value)}
mr: 1, sx={{
width: i18n.language === "en" ? 190 : 120, mr: 1,
'[role="button"]': { py: 0.65 }, width: i18n.language === "en" ? 190 : 120,
}} '[role="button"]': { py: 0.65 },
> }}
{Object.keys(orderOpts).map((opt) => ( >
<MenuItem key={opt} value={opt}> {Object.keys(orderOpts).map((opt) => (
<span style={{ fontSize: 14 }}>{t(opt)}</span> <MenuItem key={opt} value={opt}>
</MenuItem> <span style={{ fontSize: 14 }}>{t(opt)}</span>
))} </MenuItem>
</Select> ))}
</Select>
)}
<TextField <TextField
hiddenLabel hiddenLabel
@ -145,13 +176,15 @@ const ConnectionsPage = () => {
</Box> </Box>
<Box height="calc(100% - 50px)"> <Box height="calc(100% - 50px)">
{filterConn.length > 0 ? ( {filterConn.length === 0 ? (
<BaseEmpty text="No Connections" />
) : isTableLayout ? (
<ConnectionTable connections={filterConn} />
) : (
<Virtuoso <Virtuoso
data={filterConn} data={filterConn}
itemContent={(index, item) => <ConnectionItem value={item} />} itemContent={(index, item) => <ConnectionItem value={item} />}
/> />
) : (
<BaseEmpty text="No Connections" />
)} )}
</Box> </Box>
</Paper> </Paper>

View File

@ -21,14 +21,45 @@ export const atomEnableLog = atom<boolean>({
({ setSelf, onSet }) => { ({ setSelf, onSet }) => {
const key = "enable-log"; const key = "enable-log";
setSelf(localStorage.getItem(key) !== "false"); try {
setSelf(localStorage.getItem(key) !== "false");
} catch {}
onSet((newValue, _, isReset) => { onSet((newValue, _, isReset) => {
if (isReset) { try {
localStorage.removeItem(key); if (isReset) {
} else { localStorage.removeItem(key);
localStorage.setItem(key, newValue.toString()); } else {
} localStorage.setItem(key, newValue.toString());
}
} catch {}
});
},
],
});
interface IConnectionSetting {
layout: "table" | "list";
}
export const atomConnectionSetting = atom<IConnectionSetting>({
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 {}
}); });
}, },
], ],

View File

@ -68,6 +68,7 @@ declare namespace ApiType {
destinationPort: string; destinationPort: string;
destinationIP?: string; destinationIP?: string;
process?: string; process?: string;
processPath?: string;
}; };
upload: number; upload: number;
download: number; download: number;

View File

@ -552,6 +552,17 @@
prop-types "^15.8.1" prop-types "^15.8.1"
react-is "^18.2.0" 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": "@octokit/auth-token@^2.4.4":
version "2.5.0" version "2.5.0"
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36" 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" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== 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: resize-observer-polyfill@^1.5.1:
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"