Compare commits
3 Commits
dev
...
feat/suppo
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user