Compare commits

...

3 Commits

3 changed files with 102 additions and 81 deletions

View File

@@ -187,7 +187,7 @@ export const EnhancedTrafficStats = () => {
uploadTotalUnit, uploadTotalUnit,
downloadTotal, downloadTotal,
downloadTotalUnit, downloadTotalUnit,
connectionsCount: connections?.connections.length, connectionsCount: connections?.activeConnections.length,
}; };
}, [traffic, memory, connections]); }, [traffic, memory, connections]);

View File

@@ -4,12 +4,22 @@ import { mutate } from "swr";
import useSWRSubscription from "swr/subscription"; import useSWRSubscription from "swr/subscription";
import { MihomoWebSocket } from "tauri-plugin-mihomo-api"; import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
export const initConnData: IConnections = { export const initConnData: ConnectionMonitorData = {
uploadTotal: 0, uploadTotal: 0,
downloadTotal: 0, downloadTotal: 0,
connections: [], activeConnections: [],
closedConnections: [],
}; };
export interface ConnectionMonitorData {
uploadTotal: 0;
downloadTotal: 0;
activeConnections: IConnectionsItem[];
closedConnections: IConnectionsItem[];
}
const MAX_CLOSED_CONNS_NUM = 500;
export const useConnectionData = () => { export const useConnectionData = () => {
const [date, setDate] = useLocalStorage("mihomo_connection_date", Date.now()); const [date, setDate] = useLocalStorage("mihomo_connection_date", Date.now());
const subscriptKey = `getClashConnection-${date}`; const subscriptKey = `getClashConnection-${date}`;
@@ -18,7 +28,11 @@ export const useConnectionData = () => {
const wsFirstConnection = useRef<boolean>(true); const wsFirstConnection = useRef<boolean>(true);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null); const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const response = useSWRSubscription<IConnections, any, string | null>( const response = useSWRSubscription<
ConnectionMonitorData,
any,
string | null
>(
subscriptKey, subscriptKey,
(_key, { next }) => { (_key, { next }) => {
const reconnect = async () => { const reconnect = async () => {
@@ -41,28 +55,44 @@ export const useConnectionData = () => {
} else { } else {
const data = JSON.parse(msg.data) as IConnections; const data = JSON.parse(msg.data) as IConnections;
next(null, (old = initConnData) => { next(null, (old = initConnData) => {
const oldConn = old.connections; const oldConn = old.activeConnections;
const maxLen = data.connections?.length; const maxLen = data.connections?.length;
const connections: IConnectionsItem[] = []; const activeConns: IConnectionsItem[] = [];
const rest = (data.connections || []).filter((each) => { const rest = (data.connections || []).filter((each) => {
const index = oldConn.findIndex((o) => o.id === each.id); const index = oldConn.findIndex((o) => o.id === each.id);
if (index >= 0 && index < maxLen) { if (index >= 0 && index < maxLen) {
const old = oldConn[index]; const old = oldConn[index];
each.curUpload = each.upload - old.upload; each.curUpload = each.upload - old.upload;
each.curDownload = each.download - old.download; each.curDownload = each.download - old.download;
connections[index] = each; activeConns[index] = each;
return false; return false;
} }
return true; return true;
}); });
for (let i = 0; i < maxLen; ++i) { for (let i = 0; i < maxLen; ++i) {
if (!connections[i] && rest.length > 0) { if (!activeConns[i] && rest.length > 0) {
connections[i] = rest.shift()!; activeConns[i] = rest.shift()!;
connections[i].curUpload = 0; activeConns[i].curUpload = 0;
connections[i].curDownload = 0; activeConns[i].curDownload = 0;
} }
} }
return { ...data, connections }; const currentClosedConns = oldConn.filter((each) => {
const index = activeConns.findIndex(
(o) => o.id === each.id,
);
return index < 0;
});
let closedConns =
old.closedConnections.concat(currentClosedConns);
if (closedConns.length > 500) {
closedConns = closedConns.slice(-MAX_CLOSED_CONNS_NUM);
}
return {
uploadTotal: data.uploadTotal,
downloadTotal: data.downloadTotal,
activeConnections: activeConns,
closedConnections: closedConns,
} as ConnectionMonitorData;
}); });
} }
} }
@@ -109,5 +139,14 @@ export const useConnectionData = () => {
setDate(Date.now()); setDate(Date.now());
}; };
return { response, refreshGetClashConnection }; const clearClosedConnections = () => {
mutate(`$sub$${subscriptKey}`, {
uploadTotal: response.data?.uploadTotal ?? 0,
downloadTotal: response.data?.downloadTotal ?? 0,
activeConnections: response.data?.activeConnections ?? [],
closedConnections: [],
} as ConnectionMonitorData);
};
return { response, refreshGetClashConnection, clearClosedConnections };
}; };

View File

@@ -1,10 +1,16 @@
import { import {
PauseCircleOutlineRounded, Clear,
PlayCircleOutlineRounded,
TableChartRounded, TableChartRounded,
TableRowsRounded, TableRowsRounded,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { Box, Button, IconButton, MenuItem } from "@mui/material"; import {
Box,
Button,
ButtonGroup,
Fab,
IconButton,
MenuItem,
} from "@mui/material";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useCallback, useMemo, useRef, useState } from "react"; import { useCallback, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -21,28 +27,24 @@ import {
import { ConnectionItem } from "@/components/connection/connection-item"; import { ConnectionItem } from "@/components/connection/connection-item";
import { ConnectionTable } from "@/components/connection/connection-table"; import { ConnectionTable } from "@/components/connection/connection-table";
import { useConnectionData } from "@/hooks/use-connection-data"; import { useConnectionData } from "@/hooks/use-connection-data";
import { useVisibility } from "@/hooks/use-visibility";
import { useConnectionSetting } from "@/services/states"; import { useConnectionSetting } from "@/services/states";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
const initConn: IConnections = {
uploadTotal: 0,
downloadTotal: 0,
connections: [],
};
type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]; type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[];
const ConnectionsPage = () => { const ConnectionsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const pageVisible = useVisibility();
const [match, setMatch] = useState<(input: string) => boolean>( const [match, setMatch] = useState<(input: string) => boolean>(
() => () => true, () => () => true,
); );
const [curOrderOpt, setCurOrderOpt] = useState("Default"); const [curOrderOpt, setCurOrderOpt] = useState("Default");
const [connectionsType, setConnectionsType] = useState<"active" | "closed">(
"active",
);
const { const {
response: { data: connections }, response: { data: connections },
clearClosedConnections,
} = useConnectionData(); } = useConnectionData();
const [setting, setSetting] = useConnectionSetting(); const [setting, setSetting] = useConnectionSetting();
@@ -65,43 +67,23 @@ const ConnectionsPage = () => {
[], [],
); );
const [isPaused, setIsPaused] = useState(false);
const [frozenData, setFrozenData] = useState<IConnections | null>(null);
// 使用全局连接数据
const displayData = useMemo(() => {
if (!pageVisible) return initConn;
if (isPaused) {
return (
frozenData ?? {
uploadTotal: connections?.uploadTotal,
downloadTotal: connections?.downloadTotal,
connections: connections?.connections,
}
);
}
return {
uploadTotal: connections?.uploadTotal,
downloadTotal: connections?.downloadTotal,
connections: connections?.connections,
};
}, [isPaused, frozenData, connections, pageVisible]);
const [filterConn] = useMemo(() => { const [filterConn] = useMemo(() => {
const orderFunc = orderOpts[curOrderOpt]; const orderFunc = orderOpts[curOrderOpt];
let conns = displayData.connections?.filter((conn) => { const conns =
(connectionsType === "active"
? connections?.activeConnections
: connections?.closedConnections) ?? [];
let matchConns = conns.filter((conn) => {
const { host, destinationIP, process } = conn.metadata; const { host, destinationIP, process } = conn.metadata;
return ( return (
match(host || "") || match(destinationIP || "") || match(process || "") match(host || "") || match(destinationIP || "") || match(process || "")
); );
}); });
if (orderFunc) conns = orderFunc(conns ?? []); if (orderFunc) matchConns = orderFunc(matchConns ?? []);
return [conns]; return [matchConns];
}, [displayData, match, curOrderOpt, orderOpts]); }, [connections, connectionsType, match, curOrderOpt, orderOpts]);
const onCloseAll = useLockFn(closeAllConnections); const onCloseAll = useLockFn(closeAllConnections);
@@ -111,21 +93,6 @@ const ConnectionsPage = () => {
setMatch(() => match); setMatch(() => match);
}, []); }, []);
const handlePauseToggle = useCallback(() => {
setIsPaused((prev) => {
if (!prev) {
setFrozenData({
uploadTotal: connections?.uploadTotal ?? 0,
downloadTotal: connections?.downloadTotal ?? 0,
connections: connections?.connections ?? [],
});
} else {
setFrozenData(null);
}
return !prev;
});
}, [connections]);
return ( return (
<BasePage <BasePage
full full
@@ -140,10 +107,10 @@ const ConnectionsPage = () => {
header={ header={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Box sx={{ mx: 1 }}> <Box sx={{ mx: 1 }}>
{t("Downloaded")}: {parseTraffic(displayData.downloadTotal)} {t("Downloaded")}: {parseTraffic(connections?.downloadTotal)}
</Box> </Box>
<Box sx={{ mx: 1 }}> <Box sx={{ mx: 1 }}>
{t("Uploaded")}: {parseTraffic(displayData.uploadTotal)} {t("Uploaded")}: {parseTraffic(connections?.uploadTotal)}
</Box> </Box>
<IconButton <IconButton
color="inherit" color="inherit"
@@ -162,18 +129,6 @@ const ConnectionsPage = () => {
<TableChartRounded titleAccess={t("Table View")} /> <TableChartRounded titleAccess={t("Table View")} />
)} )}
</IconButton> </IconButton>
<IconButton
color="inherit"
size="small"
onClick={handlePauseToggle}
title={isPaused ? t("Resume") : t("Pause")}
>
{isPaused ? (
<PlayCircleOutlineRounded />
) : (
<PauseCircleOutlineRounded />
)}
</IconButton>
<Button size="small" variant="contained" onClick={onCloseAll}> <Button size="small" variant="contained" onClick={onCloseAll}>
<span style={{ whiteSpace: "nowrap" }}>{t("Close All")}</span> <span style={{ whiteSpace: "nowrap" }}>{t("Close All")}</span>
</Button> </Button>
@@ -194,6 +149,22 @@ const ConnectionsPage = () => {
zIndex: 2, zIndex: 2,
}} }}
> >
<ButtonGroup sx={{ mr: 1, flexBasis: "content" }}>
<Button
size="small"
variant={connectionsType === "active" ? "contained" : "outlined"}
onClick={() => setConnectionsType("active")}
>
{t("Active")} {connections?.activeConnections.length}
</Button>
<Button
size="small"
variant={connectionsType === "closed" ? "contained" : "outlined"}
onClick={() => setConnectionsType("closed")}
>
{t("Closed")} {connections?.closedConnections.length}
</Button>
</ButtonGroup>
{!isTableLayout && ( {!isTableLayout && (
<BaseStyledSelect <BaseStyledSelect
value={curOrderOpt} value={curOrderOpt}
@@ -232,6 +203,17 @@ const ConnectionsPage = () => {
/> />
)} )}
<ConnectionDetail ref={detailRef} /> <ConnectionDetail ref={detailRef} />
{connectionsType === "closed" && (
<Fab
variant="extended"
sx={{ position: "absolute", right: 16, bottom: 16 }}
color="primary"
onClick={() => clearClosedConnections()}
>
<Clear sx={{ mr: 1 }} />
{t("Clear")}
</Fab>
)}
</BasePage> </BasePage>
); );
}; };