feat: supports show connection detail
This commit is contained in:
parent
ab6374e278
commit
54e491d8bf
104
src/components/connection/connection-detail.tsx
Normal file
104
src/components/connection/connection-detail.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import dayjs from "dayjs";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Box, Button, Snackbar } from "@mui/material";
|
||||
import { deleteConnection } from "@/services/api";
|
||||
import { truncateStr } from "@/utils/truncate-str";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
export interface ConnectionDetailRef {
|
||||
open: (detail: IConnectionsItem) => void;
|
||||
}
|
||||
|
||||
export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
||||
(props, ref) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<IConnectionsItem>(null!);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: (detail: IConnectionsItem) => {
|
||||
if (open) return;
|
||||
setOpen(true);
|
||||
setDetail(detail);
|
||||
},
|
||||
}));
|
||||
|
||||
const onClose = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
message={
|
||||
detail ? (
|
||||
<InnerConnectionDetail data={detail} onClose={onClose} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface InnerProps {
|
||||
data: IConnectionsItem;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
const { metadata, rulePayload } = data;
|
||||
const chains = [...data.chains].reverse().join(" / ");
|
||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
||||
const host = metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.destinationIP}:${metadata.destinationPort}`;
|
||||
|
||||
const information = [
|
||||
{ label: "Host", value: host },
|
||||
{ label: "Download", value: parseTraffic(data.download).join(" ") },
|
||||
{ label: "Upload", value: parseTraffic(data.upload).join(" ") },
|
||||
{
|
||||
label: "DL Speed",
|
||||
value: parseTraffic(data.curDownload ?? -1).join(" ") + "/s",
|
||||
},
|
||||
{
|
||||
label: "UL Speed",
|
||||
value: parseTraffic(data.curUpload ?? -1).join(" ") + "/s",
|
||||
},
|
||||
{ label: "Chains", value: chains },
|
||||
{ label: "Rule", value: rule },
|
||||
{
|
||||
label: "Process",
|
||||
value: truncateStr(metadata.process || metadata.processPath),
|
||||
},
|
||||
{ label: "Time", value: dayjs(data.start).fromNow() },
|
||||
{ label: "Source", value: `${metadata.sourceIP}:${metadata.sourcePort}` },
|
||||
{ label: "Destination IP", value: metadata.destinationIP },
|
||||
{ label: "Type", value: `${metadata.type}(${metadata.network})` },
|
||||
];
|
||||
|
||||
const onDelete = useLockFn(async () => deleteConnection(data.id));
|
||||
|
||||
return (
|
||||
<Box sx={{ userSelect: "text" }}>
|
||||
{information.map((each) => (
|
||||
<div key={each.label}>
|
||||
<b>{each.label}</b>: <span>{each.value}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Box sx={{ textAlign: "right" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
title="Close Connection"
|
||||
onClick={() => {
|
||||
onDelete();
|
||||
onClose?.();
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -24,10 +24,11 @@ const Tag = styled("span")(({ theme }) => ({
|
||||
|
||||
interface Props {
|
||||
value: IConnectionsItem;
|
||||
onShowDetail?: () => void;
|
||||
}
|
||||
|
||||
const ConnectionItem = (props: Props) => {
|
||||
const { value } = props;
|
||||
export const ConnectionItem = (props: Props) => {
|
||||
const { value, onShowDetail } = props;
|
||||
|
||||
const { id, metadata, chains, start, curUpload, curDownload } = value;
|
||||
|
||||
@ -44,8 +45,9 @@ const ConnectionItem = (props: Props) => {
|
||||
}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{ userSelect: "text" }}
|
||||
sx={{ userSelect: "text", cursor: "pointer" }}
|
||||
primary={metadata.host || metadata.destinationIP}
|
||||
onClick={onShowDetail}
|
||||
secondary={
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap" }}>
|
||||
<Tag sx={{ textTransform: "uppercase", color: "success" }}>
|
||||
@ -71,5 +73,3 @@ const ConnectionItem = (props: Props) => {
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionItem;
|
||||
|
@ -1,37 +1,29 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useMemo, useState } from "react";
|
||||
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||
import { Snackbar } from "@mui/material";
|
||||
import { truncateStr } from "@/utils/truncate-str";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
interface Props {
|
||||
connections: IConnectionsItem[];
|
||||
onShowDetail: (data: IConnectionsItem) => void;
|
||||
}
|
||||
|
||||
const ConnectionTable = (props: Props) => {
|
||||
const { connections } = props;
|
||||
export const ConnectionTable = (props: Props) => {
|
||||
const { connections, onShowDetail } = props;
|
||||
|
||||
const [openedDetail, setOpenedDetail] = useState<IConnectionsItem | null>(
|
||||
null
|
||||
);
|
||||
const [columnVisible, setColumnVisible] = useState<
|
||||
Partial<Record<keyof IConnectionsItem, boolean>>
|
||||
>({});
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "host",
|
||||
headerName: "Host",
|
||||
flex: 200,
|
||||
minWidth: 200,
|
||||
resizable: false,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{ field: "host", headerName: "Host", flex: 220, minWidth: 220 },
|
||||
{
|
||||
field: "download",
|
||||
headerName: "Download",
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) => parseTraffic(params.value).join(" "),
|
||||
},
|
||||
{
|
||||
field: "upload",
|
||||
@ -39,18 +31,13 @@ const ConnectionTable = (props: Props) => {
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) => parseTraffic(params.value).join(" "),
|
||||
},
|
||||
{
|
||||
field: "dlSpeed",
|
||||
headerName: "DL Speed",
|
||||
align: "right",
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) =>
|
||||
parseTraffic(params.value).join(" ") + "/s",
|
||||
},
|
||||
{
|
||||
field: "ulSpeed",
|
||||
@ -58,55 +45,26 @@ const ConnectionTable = (props: Props) => {
|
||||
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: 480,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{ field: "chains", headerName: "Chains", flex: 360, minWidth: 360 },
|
||||
{ field: "rule", headerName: "Rule", flex: 300, minWidth: 250 },
|
||||
{ field: "process", headerName: "Process", flex: 480, minWidth: 480 },
|
||||
{
|
||||
field: "time",
|
||||
headerName: "Time",
|
||||
width: 120,
|
||||
flex: 120,
|
||||
minWidth: 100,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params) => dayjs(params.value).fromNow(),
|
||||
},
|
||||
{
|
||||
field: "source",
|
||||
headerName: "Source",
|
||||
width: 150,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{ field: "source", headerName: "Source", flex: 200, minWidth: 130 },
|
||||
{
|
||||
field: "destinationIP",
|
||||
headerName: "Destination IP",
|
||||
width: 125,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "type",
|
||||
headerName: "Type",
|
||||
width: 160,
|
||||
disableColumnMenu: true,
|
||||
flex: 200,
|
||||
minWidth: 130,
|
||||
},
|
||||
{ field: "type", headerName: "Type", flex: 160, minWidth: 100 },
|
||||
];
|
||||
|
||||
const connRows = useMemo(() => {
|
||||
@ -120,18 +78,16 @@ const ConnectionTable = (props: Props) => {
|
||||
host: metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.destinationIP}:${metadata.destinationPort}`,
|
||||
download: each.download,
|
||||
upload: each.upload,
|
||||
dlSpeed: each.curDownload,
|
||||
ulSpeed: each.curUpload,
|
||||
download: parseTraffic(each.download).join(" "),
|
||||
upload: parseTraffic(each.upload).join(" "),
|
||||
dlSpeed: parseTraffic(each.curDownload).join(" ") + "/s",
|
||||
ulSpeed: parseTraffic(each.curUpload).join(" ") + "/s",
|
||||
chains,
|
||||
rule,
|
||||
process: truncateStr(
|
||||
metadata.process || metadata.processPath || "",
|
||||
16,
|
||||
56
|
||||
process: truncateStr(metadata.process || metadata.processPath)?.repeat(
|
||||
10
|
||||
),
|
||||
time: each.start,
|
||||
time: dayjs(each.start).fromNow(),
|
||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||
destinationIP: metadata.destinationIP,
|
||||
type: `${metadata.type}(${metadata.network})`,
|
||||
@ -142,101 +98,15 @@ const ConnectionTable = (props: Props) => {
|
||||
}, [connections]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataGrid
|
||||
rows={connRows}
|
||||
columns={columns}
|
||||
onRowClick={(e) => setOpenedDetail(e.row.connectionData)}
|
||||
density="compact"
|
||||
sx={{ border: "none", "div:focus": { outline: "none !important" } }}
|
||||
hideFooter
|
||||
/>
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
open={Boolean(openedDetail)}
|
||||
onClose={() => setOpenedDetail(null)}
|
||||
message={
|
||||
openedDetail ? <SingleConnectionDetail data={openedDetail} /> : null
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionTable;
|
||||
|
||||
const truncateStr = (str: string, prefixLen: number, maxLen: number) => {
|
||||
if (str.length <= maxLen) return str;
|
||||
return (
|
||||
str.slice(0, prefixLen) + " ... " + str.slice(-(maxLen - prefixLen - 5))
|
||||
);
|
||||
};
|
||||
|
||||
const SingleConnectionDetail = ({ data }: { data: IConnectionsItem }) => {
|
||||
const { metadata, rulePayload } = data;
|
||||
const chains = [...data.chains].reverse().join(" / ");
|
||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
||||
const host = metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.destinationIP}:${metadata.destinationPort}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{" "}
|
||||
<b>Host</b>: <span>{host}</span>{" "}
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
<b>Download</b>: <span>{parseTraffic(data.download).join(" ")}</span>{" "}
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
<b>Upload</b>: <span>{parseTraffic(data.upload).join(" ")}</span>{" "}
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
<b>DL Speed</b>:{" "}
|
||||
<span>{parseTraffic(data.curDownload ?? -1).join(" ") + "/s"}</span>{" "}
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
<b>UL Speed</b>:{" "}
|
||||
<span>{parseTraffic(data.curUpload ?? -1).join(" ") + "/s"}</span>{" "}
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
<b>Chains</b>: <span>{chains}</span>{" "}
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
<b>Rule</b>: <span>{rule}</span>{" "}
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
<b>Process</b>: <span>{metadata.process}</span>{" "}
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
<b>ProcessPath</b>: <span>{metadata.processPath}</span>{" "}
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
<b>Time</b>: <span>{dayjs(data.start).fromNow()}</span>{" "}
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
<b>Source</b>:{" "}
|
||||
<span>{`${metadata.sourceIP}:${metadata.sourcePort}`}</span>{" "}
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
<b>Destination IP</b>: <span>{metadata.destinationIP}</span>{" "}
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
<b>Type</b>: <span>{`${metadata.type}(${metadata.network})`}</span>{" "}
|
||||
</div>
|
||||
</div>
|
||||
<DataGrid
|
||||
hideFooter
|
||||
rows={connRows}
|
||||
columns={columns}
|
||||
onRowClick={(e) => onShowDetail(e.row.connectionData)}
|
||||
density="compact"
|
||||
sx={{ border: "none", "div:focus": { outline: "none !important" } }}
|
||||
columnVisibilityModel={columnVisible}
|
||||
onColumnVisibilityModelChange={(e) => setColumnVisible(e)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import {
|
||||
Box,
|
||||
@ -18,8 +18,12 @@ import { atomConnectionSetting } from "@/services/states";
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { BaseEmpty, BasePage } from "@/components/base";
|
||||
import { useWebsocket } from "@/hooks/use-websocket";
|
||||
import ConnectionItem from "@/components/connection/connection-item";
|
||||
import ConnectionTable from "@/components/connection/connection-table";
|
||||
import { ConnectionItem } from "@/components/connection/connection-item";
|
||||
import { ConnectionTable } from "@/components/connection/connection-table";
|
||||
import {
|
||||
ConnectionDetail,
|
||||
ConnectionDetailRef,
|
||||
} from "@/components/connection/connection-detail";
|
||||
|
||||
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
|
||||
|
||||
@ -106,6 +110,8 @@ const ConnectionsPage = () => {
|
||||
|
||||
const onCloseAll = useLockFn(closeAllConnections);
|
||||
|
||||
const detailRef = useRef<ConnectionDetailRef>(null!);
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title={t("Connections")}
|
||||
@ -186,14 +192,24 @@ const ConnectionsPage = () => {
|
||||
{filterConn.length === 0 ? (
|
||||
<BaseEmpty text="No Connections" />
|
||||
) : isTableLayout ? (
|
||||
<ConnectionTable connections={filterConn} />
|
||||
<ConnectionTable
|
||||
connections={filterConn}
|
||||
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
||||
/>
|
||||
) : (
|
||||
<Virtuoso
|
||||
data={filterConn}
|
||||
itemContent={(index, item) => <ConnectionItem value={item} />}
|
||||
itemContent={(index, item) => (
|
||||
<ConnectionItem
|
||||
value={item}
|
||||
onShowDetail={() => detailRef.current?.open(item)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<ConnectionDetail ref={detailRef} />
|
||||
</Paper>
|
||||
</BasePage>
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
|
||||
const parseTraffic = (num: number) => {
|
||||
const parseTraffic = (num?: number) => {
|
||||
if (typeof num !== "number") return ["NaN", ""];
|
||||
if (num < 1000) return [`${Math.round(num)}`, "B"];
|
||||
const exp = Math.min(Math.floor(Math.log2(num) / 10), UNITS.length - 1);
|
||||
const dat = num / Math.pow(1024, exp);
|
||||
|
6
src/utils/truncate-str.ts
Normal file
6
src/utils/truncate-str.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const truncateStr = (str?: string, prefixLen = 16, maxLen = 56) => {
|
||||
if (!str || str.length <= maxLen) return str;
|
||||
return (
|
||||
str.slice(0, prefixLen) + " ... " + str.slice(-(maxLen - prefixLen - 5))
|
||||
);
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user